目录

Table of Contents

9780124104778_FC

编程语言语用学

Programming Language Pragmatics

第四版

Fourth Edition

迈克尔·斯科特

Michael L. Scott

罗彻斯特大学计算机科学系 罗彻斯特大学计算机科学系

Department of Computer Science, University of Rochester Department of Computer Science University of Rochester

图片

目录

Table of Contents

封面图片

Cover image

封面

Title page

版权页

Copyright page

关于作者

About the Author

奉献

Dedication

前言

Foreword

前言

Preface

第四版的变化

Changes in the Fourth Edition

配套网站

The Companion Site

设计与实现侧边栏

Design & Implementation Sidebars

编号和标题示例

Numbered and Titled Examples

锻炼计划

Exercise Plan

如何使用本书

How to Use the Book

补充材料

Supplemental Materials

第四版致谢

Acknowledgments for the Fourth Edition

一:基础

I: Foundations

一:基础

I: Foundations

1:简介

1: Introduction

1.1 语言设计的艺术

1.1 The Art of Language Design

1.2 编程语言范围

1.2 The Programming Language Spectrum

1.3 为什么要学习编程语言?

1.3 Why Study Programming Languages?

1.4 编译和解释

1.4 Compilation and Interpretation

1.5 编程环境

1.5 Programming Environments

1.6 编译概述

1.6 An Overview of Compilation

1.7 总结和结束语

1.7 Summary and Concluding Remarks

1.8 练习

1.8 Exercises

1.9 探索

1.9 Explorations

1.10 书目注释

1.10 Bibliographic Notes

2:编程语言语法

2: Programming Language Syntax

2.1 指定语法:正则表达式和上下文无关文法

2.1 Specifying Syntax: Regular Expressions and Context-Free Grammars

2.2 扫描

2.2 Scanning

2.3 解析

2.3 Parsing

2.4 理论基础

2.4 Theoretical Foundations

2.5 总结与结束语

2.5 Summary and Concluding Remarks

2.6 练习

2.6 Exercises

2.7 探索

2.7 Explorations

2.8 书目注释

2.8 Bibliographic Notes

3:名称、范围和绑定

3: Names, Scopes, and Bindings

3.1 绑定时间的概念

3.1 The Notion of Binding Time

3.2 对象生命周期和存储管理

3.2 Object Lifetime and Storage Management

3.3 范围规则

3.3 Scope Rules

3.4 实现范围

3.4 Implementing Scope

3.5 范围内名称的含义

3.5 The Meaning of Names within a Scope

3.6 引用环境的绑定

3.6 The Binding of Referencing Environments

3.7 宏扩展

3.7 Macro Expansion

3.8 单独编译

3.8 Separate Compilation

3.9 总结和结束语

3.9 Summary and Concluding Remarks

3.10 练习

3.10 Exercises

3.11 探索

3.11 Explorations

3.12 书目注释

3.12 Bibliographic Notes

4:语义分析

4: Semantic Analysis

4.1 语义分析器的作用

4.1 The Role of the Semantic Analyzer

4.2 属性文法

4.2 Attribute Grammars

4.3 评估属性

4.3 Evaluating Attributes

4.4 动作例程

4.4 Action Routines

4.5 属性的空间管理

4.5 Space Management for Attributes

4.6 树文法和语法树装饰

4.6 Tree Grammars and Syntax Tree Decoration

4.7 总结和结束语

4.7 Summary and Concluding Remarks

4.8 练习

4.8 Exercises

4.9 探索

4.9 Explorations

4.10 书目注释

4.10 Bibliographic Notes

5:目标机架构

5: Target Machine Architecture

二、语言设计的核心问题

II: Core Issues in Language Design

二、语言设计的核心问题

II: Core Issues in Language Design

6:控制流

6: Control Flow

6.1 表达式求值

6.1 Expression Evaluation

6.2 结构化和非结构化流程

6.2 Structured and Unstructured Flow

6.3 测序

6.3 Sequencing

6.4 选择

6.4 Selection

6.5 迭代

6.5 Iteration

6.6 递归

6.6 Recursion

6.7 不确定性

6.7 Nondeterminacy

6.8 总结和结束语

6.8 Summary and Concluding Remarks

6.9 练习

6.9 Exercises

6.10 探索

6.10 Explorations

6.11 书目注释

6.11 Bibliographic Notes

7:类型系统

7: Type Systems

7.1 概述

7.1 Overview

7.2 类型检查

7.2 Type Checking

7.3 参数多态性

7.3 Parametric Polymorphism

7.4 相等性测试和赋值

7.4 Equality Testing and Assignment

7.5 总结和结束语

7.5 Summary and Concluding Remarks

7.6 练习

7.6 Exercises

7.7 探索

7.7 Explorations

7.8 书目注释

7.8 Bibliographic Notes

8:复合类型

8: Composite Types

8.1 记录(结构)

8.1 Records (Structures)

8.2 数组

8.2 Arrays

8.3 字符串

8.3 Strings

8.4 集合

8.4 Sets

8.5 指针和递归类型

8.5 Pointers and Recursive Types

8.6 列表

8.6 Lists

8.7 文件和输入/输出

8.7 Files and Input/Output

8.8 总结和结束语

8.8 Summary and Concluding Remarks

8.9 练习

8.9 Exercises

8.10 探索

8.10 Explorations

8.11 书目注释

8.11 Bibliographic Notes

9:子程序和控制抽象

9: Subroutines and Control Abstraction

9.1 堆栈布局回顾

9.1 Review of Stack Layout

9.2 调用序列

9.2 Calling Sequences

9.3 参数传递

9.3 Parameter Passing

9.4 异常处理

9.4 Exception Handling

9.5 协程

9.5 Coroutines

9.6 事件

9.6 Events

9.7 总结和结束语

9.7 Summary and Concluding Remarks

9.8 练习

9.8 Exercises

9.9 探索

9.9 Explorations

9.10 书目注释

9.10 Bibliographic Notes

10:数据抽象和面向对象

10: Data Abstraction and Object Orientation

10.1 面向对象编程

10.1 Object-Oriented Programming

10.2 封装和继承

10.2 Encapsulation and Inheritance

10.3 初始化和终止

10.3 Initialization and Finalization

10.4 动态方法绑定

10.4 Dynamic Method Binding

10.5 混合继承

10.5 Mix-In Inheritance

10.6 真正的多重继承

10.6 True Multiple Inheritance

10.7 重新审视面向对象编程

10.7 Object-Oriented Programming Revisited

10.8 总结和结束语

10.8 Summary and Concluding Remarks

10.9 练习

10.9 Exercises

10.10 探索

10.10 Explorations

10.11 书目注释

10.11 Bibliographic Notes

III:替代编程模型

III: Alternative Programming Models

III:替代编程模型

III: Alternative Programming Models

11:函数式语言

11: Functional Languages

11.1 历史起源

11.1 Historical Origins

11.2 函数式编程概念

11.2 Functional Programming Concepts

11.3 方案概述

11.3 A Bit of Scheme

11.4 一点 OCaml

11.4 A Bit of OCaml

11.5 重新审视评估顺序

11.5 Evaluation Order Revisited

11.6 高阶函数

11.6 Higher-Order Functions

11.7 理论基础

11.7 Theoretical Foundations

11.8 函数式编程的视角

11.8 Functional Programming in Perspective

11.9 总结和结束语

11.9 Summary and Concluding Remarks

11.10 练习

11.10 Exercises

11.11 探索

11.11 Explorations

11.12 书目注释

11.12 Bibliographic Notes

12:逻辑语言

12: Logic Languages

12.1 逻辑编程概念

12.1 Logic Programming Concepts

12.2 序言

12.2 Prolog

12.3 理论基础

12.3 Theoretical Foundations

12.4 逻辑编程的视角

12.4 Logic Programming in Perspective

12.5 总结和结束语

12.5 Summary and Concluding Remarks

12.6 练习

12.6 Exercises

12.7 探索

12.7 Explorations

12.8 书目注释

12.8 Bibliographic Notes

13:并发

13: Concurrency

13.1 背景和动机

13.1 Background and Motivation

13.2 并发编程基础

13.2 Concurrent Programming Fundamentals

13.3 实现同步

13.3 Implementing Synchronization

13.4 语言级构造

13.4 Language-Level Constructs

13.5 消息传递

13.5 Message Passing

13.6 总结和结束语

13.6 Summary and Concluding Remarks

13.7 练习

13.7 Exercises

13.8 探索

13.8 Explorations

13.9 书目注释

13.9 Bibliographic Notes

14:脚本语言

14: Scripting Languages

14.1 什么是脚本语言?

14.1 What Is a Scripting Language?

14.2 问题域

14.2 Problem Domains

14.3 编写万维网脚本

14.3 Scripting the World Wide Web

14.4 创新功能

14.4 Innovative Features

14.5 总结和结束语

14.5 Summary and Concluding Remarks

14.6 练习

14.6 Exercises

14.7 探索

14.7 Explorations

14.8 书目注释

14.8 Bibliographic Notes

第四部分:进一步研究实现情况

IV: A Closer Look at Implementation

第四部分:进一步研究实现情况

IV: A Closer Look at Implementation

15:构建可运行的程序

15: Building a Runnable Program

15.1 后端编译器结构

15.1 Back-End Compiler Structure

15.2 中间形式

15.2 Intermediate Forms

15.3 代码生成

15.3 Code Generation

15.4 地址空间组织

15.4 Address Space Organization

15.5 组装

15.5 Assembly

15.6 链接

15.6 Linking

15.7 动态链接

15.7 Dynamic Linking

15.8 总结和结束语

15.8 Summary and Concluding Remarks

15.9 练习

15.9 Exercises

15.10 探索

15.10 Explorations

15.11 书目注释

15.11 Bibliographic Notes

16:运行时程序管理

16: Run-Time Program Management

16.1 虚拟机

16.1 Virtual Machines

16.2 机器代码的后期绑定

16.2 Late Binding of Machine Code

16.3 检查/自省

16.3 Inspection/Introspection

16.4 总结和结束语

16.4 Summary and Concluding Remarks

16.5 练习

16.5 Exercises

16.6 探索

16.6 Explorations

16.7 书目注释

16.7 Bibliographic Notes

17:代码改进

17: Code Improvement

A:提到的编程语言

A: Programming Languages Mentioned

B:语言设计和语言实现

B: Language Design and Language Implementation

C: 编号示例

C: Numbered Examples

参考书目

Bibliography

指数

Index

版权

Copyright

关于作者

About the Author

Michael L. Scott 是罗彻斯特大学计算机科学系的教授,曾任系主任。他于 1985 年获得威斯康星大学麦迪逊分校计算机科学博士学位。2014 年至 2015 年,他担任 Google 客座科学家。他的研究兴趣在于编程语言、操作系统和高级计算机架构的交叉领域,重点是并行和分布式计算。他与 John Mellor-Crummey 共同设计的 MCS 互斥锁被用于各种商业和学术系统。与 Maged Michael、Bill Scherer 和 Doug Lea 共同设计的其他几种算法出现在 java.util.concurrent 标准库中。2006,他和 Mellor-Crummey 博士共同获得了 ACM SIGACT/SIGOPS Edsger W. Dijkstra 分布式计算奖。

Michael L. Scott is a professor and past chair of the Department of Computer Science at the University of Rochester. He received his Ph.D. in computer sciences in 1985 from the University of Wisconsin–Madison. From 2014–2015 he was a Visiting Scientist at Google. His research interests lie at the intersection of programming languages, operating systems, and high-level computer architecture, with an emphasis on parallel and distributed computing. His MCS mutual exclusion lock, co-designed with John Mellor-Crummey, is used in a variety of commercial and academic systems. Several other algorithms, co-designed with Maged Michael, Bill Scherer, and Doug Lea, appear in the java.util.concurrent standard library. In 2006 he and Dr. Mellor-Crummey shared the ACM SIGACT/SIGOPS Edsger W. Dijkstra Prize in Distributed Computing.

Scott 博士是计算机协会会员、电气电子工程师学会会员,也是 Usenix、忧思科学家联盟和美国大学教授协会的成员。他是 150 多篇经过审查的出版物的作者,曾担任 2003 年 ACM 操作系统原理研讨会 (SOSP) 的总主席,以及 2007 年 ACM SIGPLAN 事务计算研讨会 (TRANSACT)、2008 年 ACM SIGPLAN 并行编程原理与实践研讨会 (PPoPP) 和 2012 年编程语言和操作系统架构支持国际会议 (ASPLOS) 的程序主席。2001 年,他因在本科教学方面的杰出成就和艺术性而获得罗彻斯特大学罗伯特和帕梅拉·戈尔根奖。

Dr. Scott is a Fellow of the Association for Computing Machinery, a Fellow of the Institute of Electrical and Electronics Engineers, and a member of Usenix, the Union of Concerned Scientists, and the American Association of University Professors. The author of more than 150 refereed publications, he served as General Chair of the 2003 ACM Symposium on Operating Systems Principles (SOSP) and as Program Chair of the 2007 ACM SIGPLAN Workshop on Transactional Computing (TRANSACT), the 2008 ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming (PPoPP), and the 2012 International Conference on Architectural Support for Programming Languages and Operating Systems (ASPLOS). In 2001 he received the University of Rochester's Robert and Pamela Goergen Award for Distinguished Achievement and Artistry in Undergraduate Teaching.

奉献

Dedication

致家人和朋友。

To family and friends.

前言

Foreword

编程语言被普遍认为是每个计算机科学家必须掌握的核心科目之一。原因很明显:这些语言是我们用于开发产品和交流新想法的主要符号。它们通过推动那些塑造信息时代的数百万行程序的开发,影响了该领域。它们的成功归功于计算机科学界长期以来在创建新语言和制定实现策略方面的努力。迈克尔·斯科特在本书的脚注和书目注释中提到的大量计算机科学家以及它所包含的主题的数量和多样性,清楚地体现了这种努力的规模。

Programming languages are universally accepted as one of the core subjects that every computer scientist must master. The reason is clear: these languages are the main notation we use for developing products and for communicating new ideas. They have influenced the field by enabling the development of those multimillion-line programs that shaped the information age. Their success is owed to the long-standing effort of the computer science community in the creation of new languages and in the development of strategies for their implementation. The large number of computer scientists mentioned in the footnotes and bibliographic notes in this book by Michael Scott is a clear manifestation of the magnitude of this effort as is the sheer number and diversity of topics it contains.

本书讨论了超过 75 种编程语言。它们代表了跨时代、跨范式和跨应用领域的语言设计中最佳和最具影响力的贡献。它们是数十年工作的成果,最初在 20 世纪 50 年代诞生了 Fortran 和 Lisp,随后几年又诞生了众多语言,而在我们这个时代,则诞生了用于编写 Web 程序的流行动态语言。这 75 多种语言涵盖了众多范式,包括命令式、函数式、逻辑式、静态式、动态式、顺序式、共享内存并行、分布式内存并行、数据流、高级和中间语言。它们包括用于科学计算、符号操作和访问数据库的语言。语言的丰富多样性对于程序员的工作效率至关重要,也是计算学科的一大财富。

Over 75 programming languages are discussed. They represent the best and most influential contributions in language design across time, paradigms, and application domains. They are the outcome of decades of work that led initially to Fortran and Lisp in the 1950s, to numerous languages in the years that followed, and, in our times, to the popular dynamic languages used to program the Web. The 75 plus languages span numerous paradigms including imperative, functional, logic, static, dynamic, sequential, shared-memory parallel, distributed memory parallel, dataflow, high-level, and intermediate languages. They include languages for scientific computing, for symbolic manipulations, and for accessing databases. This rich diversity of languages is crucial for programmer productivity and is one of the great assets of the discipline of computing.

本书涵盖了各种语言,详细讨论了控制流、类型和抽象机制。这些是开发组织良好、模块化、易于理解和易于维护的程序所需的表示。了解这些核心功能及其在当今语言中的体现是成为一名高效程序员和更好地理解当今计算机科学的基本基础。

Cutting across languages, this book presents a detailed discussion of control flow, types, and abstraction mechanisms. These are the representations needed to develop programs that are well organized, modular, easy to understand, and easy to maintain. Knowledge of these core features and of their incarnation in today's languages is a basic foundation to be an effective programmer and to better understand computer science today.

编程语言的实现策略必须与设计范式一起研究。原因之一是语言的成功取决于其实现的质量。此外,这些策略的能力有时会限制语言的设计。语言的实现始于解析和词汇扫描,这是计算程序句法结构所必需的。第一部分中描述的当今解析技术是有史以来最漂亮的算法之一,也是使用数学对象创建实用工具的一个很好的例子。它们值得研究作为一项智力成就。它们当然具有很大的实用价值,而欣赏这些策略的伟大之处的一个好方法是回到第一个 Fortran 编译器,研究构建该编译器的先驱者用于实现运算符优先级的临时但非常巧妙的策略。

Strategies to implement programming languages must be studied together with the design paradigms. A reason is that success of a language depends on the quality of its implementation. Also, the capabilities of these strategies sometimes constraint the design of languages. The implementation of a language starts with parsing and lexical scanning needed to compute the syntactic structure of programs. Today's parsing techniques, described in Part I, are among the most beautiful algorithms ever developed and are a great example of the use of mathematical objects to create practical instruments. They are worthwhile studying just as an intellectual achievement. They are of course of great practical value, and a good way to appreciate the greatness of these strategies is to go back to the first Fortran compiler and study the ad hoc, albeit highly ingenious, strategy used to implement precedence of operators by the pioneers that built that compiler.

实现的另一个常见组件是编译器组件,它执行从高级语言表示到适合由真实或虚拟机执行的较低级别形式的转换。转换可以提前完成,也可以在执行期间(即时)完成,或者两者兼而有之。本书讨论了这些方法和实现策略,包括由解析驱动的优雅转换机制。为了生成高效的代码,转换例程应用策略来避免冗余计算,有效利用内存层次结构,并利用处理器内并行性。这些有时相互冲突的目标由编译器的优化组件承担。虽然这个主题通常超出了编译器第一门课程的范围,但本书让读者可以在第 IV 部分中很好地了解程序优化。

The other usual component of implementation are the compiler components that carry out the translation from the high-level language representation to a lower level form suitable for execution by real or virtual machines. The translation can be done ahead of time, during execution (just in time), or both. The book discusses these approaches and implementation strategies including the elegant mechanisms of translation driven by parsing. To produce highly efficient code, translation routines apply strategies to avoid redundant computations, make efficient use of the memory hierarchy, and take advantage ofintra-processor parallelism. These, sometimes conflicting goals, are undertaken by the optimization components of compilers. Although this topic is typically outside the scope of a first course on compilers, the book gives the reader access to a good overview of program optimization in Part IV.

计算领域的一个重要最新发展是并行性的普及,人们预计在可预见的未来,性能提升将主要来自有效利用这种并行性。本书通过向读者介绍并发编程中的一系列主题(包括线程间的同步、通信和协调机制)来响应这一发展。随着并行性逐渐成为计算领域的常态,这些信息将变得越来越重要。

An important recent development in computing is the popularization of parallelism and the expectation that, in the foreseeable future, performance gains will mainly be the result of effectively exploiting this parallelism. The book responds to this development by presenting the reader with a range of topics in concurrent programming including mechanisms for synchronization, communication, and coordination across threads. This information will become increasingly important as parallelism consolidates as the norm in computing.

编程语言是程序员和机器之间的桥梁。算法必须在编程语言中表示出来才能执行。编程语言设计和实现的研究需要理解用于连接计算不同方面的策略,因此具有很大的教育价值。Michael Scott 的《编程语言语用学》对这一主题进行了如此广泛的论述,对文献的一大贡献,也是计算机科学家的宝贵信息来源。

Programming languages are the bridge between programmers and machines. It is in them that algorithms must be represented for execution. The study of programming languages design and implementation offers great educational value by requiring an understanding of the strategies used to connect the different aspects of computing. By presenting such an extensive treatment of the subject, Michael Scott’s Programming Language Pragmatics, is a great contribution to the literature and a valuable source of information for computer scientists.

David Padua,伊利诺伊大学香槟分校 Siebel 计算机科学中心 伊利诺伊大学香槟分校 Siebel 计算机科学中心

David Padua, Siebel Center for Computer Science, University of Illinois at Urbana-Champaign Siebel Center for Computer Science University of Illinois at Urbana-Champaign

前言

Preface

计算机编程课程是普通学生首次接触计算机科学领域的课程。大多数选修该课程的学生一生都在使用计算机,用于社交网络、电子邮件、游戏、网页浏览、文字处理和许多其他任务,但直到他们编写了第一个程序后,他们才开始了解应用程序的工作原理。在获得一定的程序员能力后(大概是在数据结构和算法课程的帮助下),自然而然的下一步就是想知道编程语言是如何工作的。本书对此进行了解释。它的目标很简单,就是成为最全面、最准确的语言教材,风格引人入胜,普通本科生可以理解。这一目标反映了我的信念:如果我们解释真正发生的事情,学生将更好地理解,并更享受这些材料。

A course in computer programming provides the typical student's first exposure to the field of computer science. Most students in such a course will have used computers all their lives, for social networking, email, games, web browsing, word processing, and a host of other tasks, but it is not until they write their first programs that they begin to appreciate how applications work. After gaining a certain level of facility as programmers (presumably with the help of a good course in data structures and algorithms), the natural next step is to wonder how programming languages work. This book provides an explanation. It aims, quite simply, to be the most comprehensive and accurate languages text available, in a style that is engaging and accessible to the typical undergraduate. This aim reflects my conviction that students will understand more, and enjoy the material more, if we explain what is really going on.

在传统的“系统”课程中,数据结构(可能还有计算机组织)以外的内容往往被划分为许多单独的科目,包括编程语言、编译器构造、计算机体系结构、操作系统、网络、并行和分布式计算、数据库管理系统,以及可能的软件工程、面向对象设计、图形或用户界面系统。这种划分的一个问题是,科目列表不断增加,但学士学位课程的学期数却没有增加。也许更重要的是,计算机科学中许多最有趣的发现都发生在学科之间的边界上。例如,计算机体系结构和编译器构造在 50 多年来一直相互启发,经历了一代又一代的超级计算机、流水线微处理器、多核芯片和现代 GPU。在过去十年中,虚拟化技术的进步模糊了硬件、操作系统、编译器和语言运行时系统之间的界限,并刺激了云计算的爆炸式增长。编程语言技术现在已常规嵌入从动态 Web 内容到游戏和娱乐、再到安全和金融等各个领域。

In the conventional “systems” curriculum, the material beyond data structures (and possibly computer organization) tends to be compartmentalized into a host of separate subjects, including programming languages, compiler construction, computer architecture, operating systems, networks, parallel and distributed computing, database management systems, and possibly software engineering, object-oriented design, graphics, or user interface systems. One problem with this compartmentalization is that the list of subjects keeps growing, but the number of semesters in a Bachelor's program does not. More important, perhaps, many of the most interesting discoveries in computer science occur at the boundaries between subjects. Computer architecture and compiler construction, for example, have inspired each other for over 50 years, through generations of supercomputers, pipelined microprocessors, multicore chips, and modern GPUs. Over the past decade, advances in virtualization have blurred boundaries among the hardware, operating system, compiler, and language run-time system, and have spurred the explosion in cloud computing. Programming language technology is now routinely embedded in everything from dynamic web content, to gaming and entertainment, to security and finance.

教育工作者和从业者都越来越重视这种互动。尤其是在高等教育中,核心课程的整合趋势日益明显。许多学校不再让普通学生深入学习两三门狭窄的科目,而在其他科目中留有空白,而是修改了编程语言和计算机组织课程,以涵盖更广泛的主题,并在课程中增加后续选修课各种专业。这一趋势与 ACM/IEEE-CS计算机科学课程 2013指南 [ SR13 ]非常吻合,该指南强调需要管理课程规模,并培养“系统级视角”和对理论与实践之间相互作用的理解。作者特别写道,

Increasingly, both educators and practitioners have come to emphasize these sorts of interactions. Within higher education in particular, there is a growing trend toward integration in the core curriculum. Rather than give the typical student an in-depth look at two or three narrow subjects, leaving holes in all the others, many schools have revised the programming languages and computer organization courses to cover a wider range of topics, with follow-on electives in various specializations. This trend is very much in keeping with the ACM/IEEE-CS Computer Science Curricula 2013 guidelines [SR13], which emphasize the need to manage the size of the curriculum and to cultivate both a “system-level perspective” and an appreciation of the interplay between theory and practice. In particular, the authors write,

计算机科学专业的毕业生需要在多个细节和抽象层面上进行思考。这种理解应该超越各个组件的实现细节,涵盖对计算机系统结构及其构建和分析过程的理解 [p. 24]。

Graduates of a computer science program need to think at multiple levels of detail and abstraction. This understanding should transcend the implementation details of the various components to encompass an appreciation for the structure of computer systems and the processes involved in their construction and analysis [p. 24].

关于这篇文章的具体主题,他们写道

On the specific subject of this text, they write

编程语言是程序员精确描述概念、制定算法和推理解决方案的媒介。在职业生涯中,计算机科学家将使用多种不同的语言,无论是单独使用还是一起使用。软件开发人员必须了解不同语言背后的编程模型,并在支持多种互补方法的语言中做出明智的设计选择。计算机科学家经常需要学习新的语言和编程结构,并且必须了解编程语言特性的定义、组成和实现背后的原理。有效使用编程语言并了解其局限性还需要具备编程语言翻译和静态程序分析的基本知识,以及内存管理等运行时组件 [p. 155]。

Programming languages are the medium through which programmers precisely describe concepts, formulate algorithms, and reason about solutions. In the course of a career, a computer scientist will work with many different languages, separately or together. Software developers must understand the programming models underlying different languages and make informed design choices in languages supporting multiple complementary approaches. Computer scientists will often need to learn new languages and programming constructs, and must understand the principles underlying how programming language features are defined, composed, and implemented. The effective use of programming languages, and appreciation of their limitations, also requires a basic knowledge of programming language translation and static program analysis, as well as run-time components such as memory management [p. 155].

《编程语言语用学》 (PLP)的前三版有幸顺应了综合理解的潮流。第四版延续并强化了“系统视角”,同时仍将重点放在编程语言设计上。

The first three editions of Programming Language Pragmatics (PLP) had the good fortune of riding the trend toward integrated understanding. This fourth edition continues and strengthens the “systems perspective” while preserving the central focus on programming language design.

从本质上讲,PLP 是一本关于编程语言工作原理的书。它没有列举许多不同语言的细节,而是专注于学生可能遇到的所有语言的基本概念,用各种具体的例子来说明这些概念,并探索解释不同语言为何以不同方式设计的权衡。同样,PLP 也没有解释如何构建编译器或解释器(很少有程序员会完全承担这项任务),而是专注于编译器对输入程序的作用及其原因。因此,语言设计和实现是一起探讨的,重点是它们之间的相互作用方式。

At its core, PLP is a book about how programming languages work. Rather than enumerate the details of many different languages, it focuses on concepts that underlie all the languages the student is likely to encounter, illustrating those concepts with a variety of concrete examples, and exploring the tradeoffs that explain why different languages were designed in different ways. Similarly, rather than explain how to build a compiler or interpreter (a task few programmers will undertake in its entirety), PLP focuses on what a compiler does to an input program, and why. Language design and implementation are thus explored together, with an emphasis on the ways in which they interact.

第四版的变化

Changes in the Fourth Edition

与第三版相比,PLP-4e 包括

In comparison to the third edition, PLP-4e includes

1. 新增了关于类型系统和复合类型的章节,取代了之前关于类型的单独章节

1. New chapters devoted to type systems and composite types, in place of the older single chapter on types

2. 更新了函数式编程的处理方式,并广泛涵盖了 OCaml

2. Updated treatment of functional programming, with extensive coverage of OCaml

3. 该领域变化的许多其他反映

3. Numerous other reflections of changes in the field

4. 受讲师反馈或对熟悉主题的重新思考启发而做出的改进

4. Improvements inspired by instructor feedback or a fresh consideration of familiar topics

此列表中的第 1 项可能是最明显的变化。第 7 章是以前版本中最长的,主题材料自然而然地被拆分。PLP-4e 重新组织了这些材料,让我们有机会更加明确地关注类型推断的主题,尤其是它在 ML 系列语言中的作用。它还促进了参数多态性材料的更新和重新组织,这些材料以前分散在几个不同的章节中。

Item 1 in this list is perhaps the most visible change. Chapter 7 was the longest in previous editions, and there is a natural split in the subject material. Reorganization of this material for PLP-4e afforded an opportunity to devote more explicit attention to the subject of type inference, and of its role in ML-family languages in particular. It also facilitated an update and reorganization of the material on parametric polymorphism, which was previously scattered across several different chapters and sections.

第 2 项反映了函数式技术在主流命令式语言中的日益普及,以及 SML、OCaml 和 Haskell 在教育和工业领域的日益突出地位。在整篇文章中,OCaml 现在与 Scheme 一样,都是函数式编程示例的来源。如上一段所述,ML 类型系统有一个扩展部分 (7.2.4) ,第 11.4 节包括 OCaml 概述,涵盖了相等和排序、绑定和 lambda 表达式、类型构造函数、模式匹配以及控制流和副作用。选择 OCaml 而不是 Haskell 作为 ML 系列示例反映了其在工业领域的突出地位,同时课堂经验表明——至少对于许多学生来说——在急切求值的背景下,最初接触函数式思维会更容易。对于那些希望我选择 Haskell 的同事,我深表歉意!

Item 2 reflects the increasing adoption of functional techniques into mainstream imperative languages, as well as the increasing prominence of SML, OCaml, and Haskell in both education and industry. Throughout the text, OCaml is now co-equal with Scheme as a source of functional programming examples. As noted in the previous paragraph, there is an expanded section (7.2.4) on the ML type system, and Section 11.4 includes an OCaml overview, with coverage of equality and ordering, bindings and lambda expressions, type constructors, pattern matching, and control flow and side effects. The choice of OCaml, rather than Haskell, as the ML-family exemplar reflects its prominence in industry, together with classroom experience suggesting that—at least for many students—the initial exposure to functional thinking is easier in the context of eager evaluation. To colleagues who wish I'd chosen Haskell, my apologies!

其他新材料(第 3 项)出现在整篇文章中。在适当的地方,都引用了最新语言和标准的特性,包括 C 和 C++11、Java 8、C# 5、Scala、Go、Swift、Python 3 和 HTML 5。第 3.6.4 节汇集了之前分散的 lambda 表达式介绍,并展示了如何将它们添加到各种命令式语言中。第 10.4.4 节补充了对象闭包,包括 C++11 的std::functionstd::bind。第 c-5.4.5 节介绍了 x86-64 和 ARM 体系结构,以取代以前版本中使用的 x86-32 和 MIPS。使用这两种相同体系结构的示例随后出现在调用序列(9.2)和链接(15.6)部分。x86 调用序列的介绍继续依赖于gcc;ARM 案例研究使用 LLVM。第 8.5.3 节介绍了智能指针。第 9.3.1 节中出现了 R 值引用。第 9.6.2 节的图形示例中,JavaFX 取代了 Swing 。附录 A中新增了 Go、Lua、Rust、Scala 和 Swift 的条目。

Other new material (Item 3) appears throughout the text. Wherever appropriate, reference has been made to features of the latest languages and standards, including C & C++11, Java 8, C# 5, Scala, Go, Swift, Python 3, and HTML 5. Section 3.6.4 pulls together previously scattered coverage of lambda expressions, and shows how these have been added to various imperative languages. Complementary coverage of object closures, including C++11's std::function and std::bind, appears in Section 10.4.4. Section c-5.4.5 introduces the x86-64 and ARM architectures in place of the x86-32 and MIPS used in previous editions. Examples using these same two architectures subsequently appear in the sections on calling sequences (9.2) and linking (15.6). Coverage of the x86 calling sequence continues to rely on gcc; the ARM case study uses LLVM. Section 8.5.3 introduces smart pointers. R-value references appear in Section 9.3.1. JavaFX replaces Swing in the graphics examples of Section 9.6.2. Appendix A has new entries for Go, Lua, Rust, Scala, and Swift.

最后,第 4 项包含对文本几乎每个部分的改进。更新较多的主题包括 FOLLOW 和 PREDICT 集(第 2.3.3 节);Wirth 的递归下降错误恢复算法(第 c-2.3.5 节);重载(第 3.5.2 节);模块(第 3.3.4 节);鸭子类型(第 7.3 节);记录和变体(第 8.1 节);侵入式列表(从第 10 章的运行示例中删除);静态字段和方法(第 10.2.2 节);混合继承(从配套站点移回正文,并更新以涵盖 Scala 特征和 Java 8 默认方法);多核处理器(第 13 章的普遍变化);移相器(第 13.3.1 节);内存模型(第 13.3.3 节);信号量(第 13.3.5 节);未来(第 13.4.5 节);GIMPLE 和 RTL(第 c-15.2.1节);QEMU(第 16.2.2 节);DWARF(第 16.3.2 节);以及语言谱系(图 A.1)。

Finally, Item 4 encompasses improvements to almost every section of the text. Among the more heavily updated topics are FOLLOW and PREDICT sets (Section 2.3.3); Wirth's error recovery algorithm for recursive descent (Section c-2.3.5); overloading (Section 3.5.2); modules (Section 3.3.4); duck typing (Section 7.3); records and variants (Section 8.1); intrusive lists (removed from the running example of Chapter 10); static fields and methods (Section 10.2.2); mix-in inheritance (moved from the companion site back into the main text, and updated to cover Scala traits and Java 8 default methods); multicore processors (pervasive changes to Chapter 13); phasers (Section 13.3.1); memory models (Section 13.3.3); semaphores (Section 13.3.5); futures (Section 13.4.5); GIMPLE and RTL (Section c-15.2.1); QEMU (Section 16.2.2); DWARF (Section 16.3.2); and language genealogy (Figure A.1).

为了容纳新材料,一些主题的覆盖范围已被压缩甚至删除。例如模块(第 3 章和第 10 章)、变体记录和 with 语句(第 8 章)以及元循环解释(第 11 章)。附加材料(尤其是公共语言基础结构 (CLI))已移至配套站点。在整篇文章中,来自不再广泛使用的语言的示例已在适当的情况下用更新的等效语言替换。几乎所有剩余的对 Pascal 和 Modula 的引用都只是历史性的。对 Occam 和 Tcl 的大部分介绍也已被删除。

To accommodate new material, coverage of some topics has been condensed or even removed. Examples include modules (Chapters 3 and 10), variant records and with statements (Chapter 8), and metacircular interpretation (Chapter 11). Additional material—the Common Language Infrastructure (CLI) in particular—has moved to the companion site. Throughout the text, examples drawn from languages no longer in widespread use have been replaced with more recent equivalents wherever appropriate. Almost all remaining references to Pascal and Modula are merely historical. Most coverage of Occam and Tcl has also been dropped.

总体而言,印刷文本增加了约 40 页。增加了 5 个“设计与实现”边栏、35 个编​​号示例以及约 25 个新的章末练习和探索。我们投入了大量精力来创建一致且全面的索引。与之前的版本一样,Morgan Kaufmann 一直致力于以合理的价格提供权威文本:PLP-4e 比竞争替代品便宜得多,但内容更丰富、更全面。

Overall, the printed text has grown by roughly 40 pages. There are 5 more “Design & Implementation” sidebars, 35 more numbered examples, and about 25 new end-of-chapter exercises and explorations. Considerable effort has been invested in creating a consistent and comprehensive index. As in earlier editions, Morgan Kaufmann has maintained its commitment to providing definitive texts at reasonable cost: PLP-4e is far less expensive than competing alternatives, but larger and more comprehensive.

配套网站

The Companion Site

为了尽量减少文本的物理尺寸,为新材料腾出空间,并让学生在浏览时专注于基础知识,可以在配套网站上找到超过 350 页的更高级或外围材料:booksite.elsevier.com/web/9780124104099。每个配套网站 (CS) 部分在正文中都由主题的简短介绍和总结省略材料的“更深入”段落表示。

To minimize the physical size of the text, make way for new material, and allow students to focus on the fundamentals when browsing, over 350 pages of more advanced or peripheral material can be found on a companion web site: booksite.elsevier.com/web/9780124104099. Each companion-site (CS) section is represented in the main text by a brief introduction to the subject and an “In More Depth” paragraph that summarizes the elided material.

请注意,将材料放在配套网站上并不构成对其技术重要性的判断。它只是反映了一个事实,即值得介绍的材料比一卷书或一学期课程所能容纳的要多。由于偏好和教学大纲各不相同,大多数教师可能希望从 CS 中指定阅读材料,并且大多数教师不会指定印刷文本的某些部分。我的目的是保留印刷版中可能在最多课程中涵盖的材料。

Note that placement of material on the companion site does not constitute a judgment about its technical importance. It simply reflects the fact that there is more material worth covering than will fit in a single volume or a single-semester course. Since preferences and syllabi vary, most instructors will probably want to assign reading from the CS, and most will refrain from assigning certain sections of the printed text. My intent has been to retain in print the material that is likely to be covered in the largest number of courses.

CS 中还包括指向在线资源的指针和文本中所有重要代码片段的可编译副本(超过二十种语言)。

Also included on the CS are pointers to on-line resources and compilable copies of all significant code fragments found in the text (in more than two dozen languages).

设计与实现侧边栏

Design & Implementation Sidebars

与其前身一样,PLP-4e 非常重视语言设计对实现选项的限制,以及预期实现对语言设计的影响。其中许多联系和相互作用在 140 个“设计与实现”边栏中进行了重点介绍。边栏 1.1 中有更详细的介绍。附录 B中有编号列表。

Like its predecessors, PLP-4e places heavy emphasis on the ways in which language design constrains implementation options, and the ways in which anticipated implementations have influenced language design. Many of these connections and interactions are highlighted in some 140 “Design & Implementation” sidebars. A more detailed introduction appears in Sidebar 1.1. A numbered list appears in Appendix B.

编号和标题示例

Numbered and Titled Examples

PLP-4e 中的示例与演示流程紧密结合。为了便于查找特定示例、记住其内容并在其他上下文中引用它们,每个示例的编号和标题都显示在边注中。正文和 CS 中有 1000 多个此类示例。详细列表见附录C。

Examples in PLP-4e are intimately woven into the flow of the presentation. To make it easier to find specific examples, to remember their content, and to refer to them in other contexts, a number and a title for each is displayed in a marginal note. There are over 1000 such examples across the main text and the CS. A detailed list appears in Appendix C.

锻炼计划

Exercise Plan

复习题在正文中每隔大约 10 页出现一次,位于主要章节的末尾。这些复习题直接基于前面的内容,并有简短、直接的答案。

Review questions appear throughout the text at roughly 10-page intervals, at the ends of major sections. These are based directly on the preceding material, and have short, straightforward answers.

每章末尾都有更详细的问题。这些问题分为练习探索。前者通常比每节复习问题更具挑战性,适合家庭作业或简短的项目。后者更开放,需要网络或图书馆研究、大量时间投入或形成主观意见。注册教师可以从受密码保护的网站获取许多练习(但不包括探索)的解决方案:请访问textbooks.elsevier.com/web/9780124104099

More detailed questions appear at the end of each chapter. These are divided into Exercises and Explorations. The former are generally more challenging than the per-section review questions, and should be suitable for homework or brief projects. The latter are more open-ended, requiring web or library research, substantial time commitment, or the development of subjective opinion. Solutions to many of the exercises (but not the explorations) are available to registered instructors from a password-protected web site: visit textbooks.elsevier.com/web/9780124104099.

如何使用本书

How to Use the Book

《编程语言语用学》涵盖了计算机课程 2013报告 [ SR13 ]中 PL“知识单元”的几乎所有材料。本书的设计对象是罗彻斯特大学的语言课程,该课程实际上是报告中的特色“课程范例”之一(第 369-371 页)。图 1说明了文本中的几种可能路径。

Programming Language Pragmatics covers almost all of the material in the PL “knowledge units” of the Computing Curricula 2013 report [SR13]. The languages course at the University of Rochester, for which this book was designed, is in fact one of the featured “course exemplars” in the report (pp. 369–371). Figure 1illustrates several possible paths through the text.

f22-01-9780124104099
图 1 文本路径。深色阴影区域表示配套网站上的补充“更深入”部分。与补充材料不对应的部分会显示章节编号。

对于自学或全年课程(图 1中的F轨道),我建议从头到尾学习本书,在遇到每个“更深入”部分时转向配套网站。罗切斯特的一学期课程(R轨道)也涵盖了本书的大部分内容,但遗漏了大部分 CS章节,以及自下而上的解析(2.3.4)、逻辑语言(第 12 章)和第 15 章(构建可运行程序)和第 16 章(运行时程序管理)的后半部分。请注意,函数式编程(特别是第 11 章)的材料可以用 OCaml 或 Scheme 来教授。

For self-study, or for a full-year course (track F in Figure 1), I recommend working through the book from start to finish, turning to the companion site as each “In More Depth” section is encountered. The one-semester course at Rochester (track R) also covers most of the book, but leaves out most of the CS sections, as well as bottom-up parsing (2.3.4), logic languages (Chapter 12), and the second halves of Chapters 15 (Building a Runnable Program) and 16 (Runtime Program Management). Note that the material on functional programming (Chapter 11 in particular) can be taught in either OCaml or Scheme.

某些章节(2、4、5、15、16、17 比其他章节更侧重于实现问题。这些章节可以相对于更面向设计的章节进行一定程度的重新排序。许多学生已经熟悉第 5 章中的大部分材料可能来自计算机组织课程;因此将本章放在配套网站上。一些学生可能还熟悉第2 章中的部分材料,也许来自自动机理论课程。本章的大部分内容也可以快速阅读,也许停下来思考一些实际问题,如从语法错误中恢复,或者扫描器与经典有限自动机的不同之处。

Some chapters (2, 4, 5, 15,16, 17) have a heavier emphasis than others on implementation issues. These can be reordered to a certain extent with respect to the more design-oriented chapters. Many students will already be familiar with much of the material in Chapter 5, most likely from a course on computer organization; hence the placement of the chapter on the companion site. Some students may also be familiar with some of the material in Chapter 2, perhaps from a course on automata theory. Much of this chapter can then be read quickly as well, pausing perhaps to dwell on such practical issues as recovery from syntax errors, or the ways in which a scanner differs from a classical finite automaton.

传统的编程语言课程(图 1中的P轨道)可能会省略所有扫描和解析,以及第 4 章的全部内容。它还会从头到尾淡化面向实现的材料。取而代之的是,它可以添加面向设计的 CS 部分,如多重继承(10.6)、Smalltalk(10.7.1)、lambda 演算(11.7)和谓词演算(12.3)。

A traditional programming languages course (track P in Figure 1) might leave out all of scanning and parsing, plus all of Chapter 4. It would also de-emphasize the more implementation-oriented material throughout. In place of these, it could add such design-oriented CS sections as multiple inheritance (10.6), Smalltalk (10.7.1), lambda calculus (11.7), and predicate calculus (12.3).

一些学校还使用 PLP 作为编译器入门课程(图 1中的C部分)。典型的教学大纲省略了第 III 部分(第 11 章第 14 章)的大部分内容,并且从头到尾都淡化了更注重设计的材料。取而代之的是,它包括了所有扫描和解析内容、第 15 章第 17章,以及略有不同的其他 CS 部分。

PLP has also been used at some schools for an introductory compiler course (track C in Figure 1). The typical syllabus leaves out most of Part III (Chapters 11 through 14), and de-emphasizes the more design-oriented material throughout. In place of these, it includes all of scanning and parsing, Chapters 15 through 17, and a slightly different mix of other CS sections.

对于采用学季制的学校,一个有吸引力的选择是提供一门为期一个学季的入门课程和两门可选的后续课程(图 1中的Q轨道)。入门学季可能涵盖第 1、3、6、7 和 8 章的主要(非 CS)部分,以及第29 章半部分。面向语言的后续学季可能涵盖第 9 章的其余部分、全部第 III 部分、第 6 至第 9 章的 CS 部分,以及可能形式语义、类型理论或其他相关主题的补充材料。面向编译器的后续学季可能涵盖第 2 章的其余部分;第 4第 5 章和15第 17章、第 3和第 9至第10的 CS 部分,以及可能关于自动代码生成、积极代码改进、编程工具等的补充材料。

For a school on the quarter system, an appealing option is to offer an introductory one-quarter course and two optional follow-on courses (track Q in Figure 1). The introductory quarter might cover the main (non-CS) sections of Chapters 1, 3, 6, 7, and 8, plus the first halves of Chapters 2 and 9. A language-oriented follow-on quarter might cover the rest of Chapter 9, all of Part III, CS sections from Chapters 6 through 9, and possibly supplemental material on formal semantics, type theory, or other related topics. A compiler-oriented follow-on quarter might cover the rest of Chapter 2; Chapters 45 and 1517, CS sections from Chapters 3 and 910, and possibly supplemental material on automatic code generation, aggressive code improvement, programming tools, and so on.

无论以何种方式阅读本书,我都假定读者已经对至少一种命令式语言有了丰富的使用经验。具体是哪种语言并不重要。书中的示例来自多种语言,但都附有足够的注释和其他讨论,即使没有相关经验的读者也应该能够轻松理解。附录 A中提供了 60 多种不同语言的单段介绍。如果需要,算法将以非正式的伪代码形式呈现,这些伪代码应该是不言自明的。真正的编程语言代码采用“打字机”字体。伪代码采用无衬线字体

Whatever the path through the text, I assume that the typical reader has already acquired significant experience with at least one imperative language. Exactly which language it is shouldn't matter. Examples are drawn from a wide variety of languages, but always with enough comments and other discussion that readers without prior experience should be able to understand easily. Single-paragraph introductions to more than 60 different languages appear in Appendix A. Algorithms, when needed, are presented in an informal pseudocode that should be self-explanatory. Real programming language code is set in “typewriter“ font. Pseudocode is set in a sans-serif font.

补充材料

Supplemental Materials

除了补充章节外,配套网站还包含所有非平凡示例的完整源代码,以及书中所有已知错误的列表。其他资源可在线获取,网址为textbooks.elsevier.com/web/9780124104099。对于采用该文本的教师,密码保护页面可访问

In addition to supplemental sections, the companion site contains complete source code for all nontrivial examples, and a list of all known errors in the book. Additional resources are available on-line at textbooks.elsevier.com/web/9780124104099. For instructors who have adopted the text, a password-protected page provides access to

 书中所有图表的 PDF 源代码均可编辑

 Editable PDF source for all the figures in the book

 可编辑的 PowerPoint 幻灯片

 Editable PowerPoint slides

 大部分练习的答案

 Solutions to most of the exercises

 对大型项目的建议

 Suggestions for larger projects

第四版致谢

Acknowledgments for the Fourth Edition

在编写第四版的过程中,我得到了很多人的慷慨帮助。许多人对第三版提供了勘误表或其他反馈,其中包括 Yacine Belkadi、Björn Brandenburg、Bob Cochran、Daniel Crisman、Marcelino Debajo、Chen Ding、Peter Drake、Michael Edgar、Michael Glass、Sergio Gomes、Allan Gottlieb、Hossein Hadavi、Chris Hart、Thomas Helmuth、Wayne Heym、Scott Hoge、Kelly Jones、Ahmed Khademzadeh、Eleazar Enrique Leal、Kyle Liddell、Annie Liu、Hao Luo、Dirk Müller、Holger Peine、Andreas Priesnitz、Mikhail Prokharau、Harsh Raju 和 Jingguo Yao。我还非常感谢在之前版本中感谢的许多人,以及使这些版本取得成功的评论者、采用者和读者。

In preparing the fourth edition, I have been blessed with the generous assistance of a very large number of people. Many provided errata or other feedback on the third edition, among them Yacine Belkadi, Björn Brandenburg, Bob Cochran, Daniel Crisman, Marcelino Debajo, Chen Ding, Peter Drake, Michael Edgar, Michael Glass, Sergio Gomes, Allan Gottlieb, Hossein Hadavi, Chris Hart, Thomas Helmuth, Wayne Heym, Scott Hoge, Kelly Jones, Ahmed Khademzadeh, Eleazar Enrique Leal, Kyle Liddell, Annie Liu, Hao Luo, Dirk Müller, Holger Peine, Andreas Priesnitz, Mikhail Prokharau, Harsh Raju, and Jingguo Yao. I also remain indebted to the many individuals acknowledged in previous editions, and to the reviewers, adopters, and readers who made those editions a success.

第四版的匿名审阅者提供了大量有用的建议;我向你们所有人表示感谢!特别感谢麻省理工学院的 Adam Chlipala 对函数式编程内容的详细而深刻的建议。我还要感谢 Nelson Beebe(犹他大学),他指出编译器不能安全地对可能是 NaN 的浮点数使用整数比较;感谢 Dan Scarafoni 提示我在生成 PREDICT 集的算法中区分符号的 FIRST/EPS 和字符串的 FIRST/EPS;感谢 Dave Musicant 建议改进深度绑定的描述;感谢 Allan Gottlieb(纽约大学)对 Ada 语义的几个关键澄清;感谢 Benjamin Kowarsch 对 Objective-C 的类似澄清。所有这些领域中仍然存在的问题完全是我自己的问题。

Anonymous reviewers for the fourth edition provided a wealth of useful suggestions; my thanks to all of you! Special thanks to Adam Chlipala of MIT for his detailed and insightful suggestions on the coverage of functional programming. My thanks as well to Nelson Beebe (University of Utah) for pointing out that compilers cannot safely use integer comparisons for floating-point numbers that may be NaNs; to Dan Scarafoni for prompting me to distinguish between FIRST/EPS of symbols and FIRST/EPS of strings in the algorithm to generate PREDICT sets; to Dave Musicant for suggested improvements to the description of deep binding; to Allan Gottlieb (NYU) for several key clarifications regarding Ada semantics; and to Benjamin Kowarsch for similar clarifications regarding Objective-C. Problems that remain in all these areas are entirely my own.

在编写第四版时,我借鉴了在罗彻斯特大学向高年级本科生教授此教材的 25 年经验。我感谢所有学生的热情和反馈。我还要感谢我的同事和研究生,以及系里的行政、秘书和技术人员,感谢他们提供了如此支持和高效的工作环境。最后,我要感谢 David Padua,自从读研究生以来,我一直很钦佩他的作品;我很荣幸他能成为序言的作者。

In preparing the fourth edition, I have drawn on 25 years of experience teaching this material to upper-level undergraduates at the University of Rochester. I am grateful to all my students for their enthusiasm and feedback. My thanks as well to my colleagues and graduate students, and to the department's administrative, secretarial, and technical staff for providing such a supportive and productive work environment. Finally, my thanks to David Padua, whose work I have admired since I was in graduate school; I am deeply honored to have him as the author of the Foreword.

正如之前的版本一样,与 Morgan Kaufmann 的员工合作是一种真正的乐趣,无论是在专业层面还是个人层面。我特别感谢高级开发编辑 Nate McFadden,他以无尽的耐心、幽默感和对细节的敏锐眼光指导了这一版和前两版;感谢管理本书制作的 Mohana Natarajan;感谢出版商 Todd Green,他在更大的 Elsevier 世界中保持了 Morgan Kauffman 印记的个人风格。

As they were on previous editions, the staff at Morgan Kaufmann has been a genuine pleasure to work with, on both a professional and a personal level. My thanks in particular to Nate McFadden, Senior Development Editor, who shepherded both this and the previous two editions with unfailing patience, good humor, and a fine eye for detail; to Mohana Natarajan, who managed the book's production; and to Todd Green, Publisher, who upholds the personal touch of the Morgan Kauffman imprint within the larger Elsevier universe.

最重要的是,我要感谢我的妻子凯利,感谢她在我写作和修改的漫长岁月中给予我的耐心和支持。计算机是一项很好的职业,但家庭才是最重要的。

Most important, I am indebted to my wife, Kelly, for her patience and support through endless months of writing and revising. Computing is a fine profession, but family is what really matters.

迈克尔·斯科特

Michael L. Scott

纽约州罗切斯特

Rochester, NY

2015 年 8 月

August 2015

基础

Foundations

基础

Foundations

编程语言语用学的一个核心前提是语言设计和实现密切相关;很难只研究其中一个而不研究另一个。

A central premise of Programming Language Pragmatics is that language design and implementation are intimately connected; it's hard to study one without the other.

本书的大部分内容(第二部分和第三部分)是围绕语言设计的主题展开的,但详细介绍了设计决策如何受到实现问题的影响的多种方式。

The bulk of the text—Parts II and III—is organized around topics in language design, but with detailed coverage throughout of the many ways in which design decisions have been shaped by implementation concerns.

前五章(第一部分)通过介绍设计和实现方面的基础材料奠定了基础。第 1 章激发了对编程语言的研究,介绍了主要的语言系列,并概述了编译过程。第 3 章介绍了程序的高级结构,重点介绍了名称、名称与对象的绑定以及控制在任何给定时间哪些绑定处于活动状态的范围规则。在此过程中,它涉及存储管理;子例程、模块和类;多态性;以及单独编译。

The first five chapters—Part I—set the stage by covering foundational material in both design and implementation. Chapter 1 motivates the study of programming languages, introduces the major language families, and provides an overview of the compilation process. Chapter 3 covers the high-level structure of programs, with an emphasis on names, the binding of names to objects, and the scope rules that govern which bindings are active at any given time. In the process it touches on storage management; subroutines, modules, and classes; polymorphism; and separate compilation.

第 2、4 和 5 章更侧重实现。它们提供了理解第 II 和第 III 部分中提到的实现问题所需的背景知识。2 章讨论了程序的语法或文本结构。它介绍了设计人员用来描述程序语法的正则表达式上下文无关文法,以及编译器或解释器用来识别该语法的扫描解析算法。在理解了语法之后,第 4 章解释了编译器(或解释器)如何确定程序的语义或含义。讨论围绕属性文法的概念展开,属性文法用于将程序映射到其他有意义的东西上,例如数学或其他现有语言。最后,第 5 章(完全在配套网站上)概述了汇编级计算机体系结构,重点介绍了与编译器最相关的现代微处理器的功能。了解这些功能的程序员不仅更有可能理解他们使用的语言为什么是这样设计的,而且还能尽可能充分有效地使用这些语言。

Chapters 2, 4, and 5 are more implementation oriented. They provide the background needed to understand the implementation issues mentioned in Parts II and III. Chapter 2 discusses the syntax, or textual structure, of programs. It introduces regular expressions and context-free grammars, which designers use to describe program syntax, together with the scanning and parsing algorithms that a compiler or interpreter uses to recognize that syntax. Given an understanding of syntax, Chapter 4 explains how a compiler (or interpreter) determines the semantics, or meaning of a program. The discussion is organized around the notion of attribute grammars, which serve to map a program onto something else that has meaning, such as mathematics or some other existing language. Finally, Chapter 5 (entirely on the companion site) provides an overview of assembly-level computer architecture, focusing on the features of modern microprocessors most relevant to compilers. Programmers who understand these features have a better chance not only of understanding why the languages they use were designed the way they were, but also of using those languages as fully and effectively as possible.

1

介绍

Introduction

例 1.1

Example 1.1

x86 机器语言的 GCD 程序

GCD program in x86 machine language

第一台电子计算机是庞然大物,占据了好几个房间,耗电量相当于一个大型工厂,耗资高达 20 世纪 40 年代的数百万美元(但计算能力却比最简单的现代手机还要低得多)。使用这些机器的程序员认为计算机的时间比他们自己的时间更宝贵。他们用机器语言编程。机器语言是直接控制处理器的位序列,使处理器在适当的时候执行加法、比较法、将数据从一个地方移动到另一个地方等操作。以这种详细程度指定程序是一项极其繁琐的任务。以下程序使用欧几里得算法计算两个整数的最大公约数 (GCD)。它是用机器语言编写的,在这里表示为十六进制(基数 16)数字,适用于 x86 指令集。

The first electronic computers were monstrous contraptions, filling several rooms, consuming as much electricity as a good-size factory, and costing millions of 1940s dollars (but with much less computing power than even the simplest modern cell phone). The programmers who used these machines believed that the computer's time was more valuable than theirs. They programmed in machine language. Machine language is the sequence of bits that directly controls a processor, causing it to add, compare, move data from one place to another, and so forth at appropriate times. Specifying programs at this level of detail is an enormously tedious task. The following program calculates the greatest common divisor (GCD) of two integers, using Euclid's algorithm. It is written in machine language, expressed here as hexadecimal (base 16) numbers, for the x86 instruction set.

5589e55383电子商务0483e4f0e83100000089c3e82a00
0000三十九c374108db600000000三十九c37e十三二十九c3三十九c3
75f6891c24e86e0000008b5天足球俱乐部c9c3二十九d8埃布埃布90

t0010

例 1.2

Example 1.2

x86 汇编程序中的 GCD 程序

GCD program in x86 assembler

随着人们开始编写更大的程序,很快就发现需要一种不容易出错的符号。汇编语言的发明是为了允许用助记符缩写来表达操作。我们的 GCD 程序在 x86 汇编语言中如下所示:

As people began to write larger programs, it quickly became apparent that a less error-prone notation was required. Assembly languages were invented to allow operations to be expressed with mnemonic abbreviations. Our GCD program looks like this in x86 assembly language:

推杆%ebp杰勒
移动%esp,%ebp子类%eax,%ebx
推杆%ebx乙:放大%eax,%ebx
子类$4, %特别杰奈一个
安德尔$−16,%esp回答:移动%ebx,(%esp)
称呼获得称呼普京
移动%eax,%ebx移动−4(%ebp),%ebx
称呼获得离开
放大%eax,%ebx保留
答案:子类%ebx,%eax
一个:放大%eax,%ebx跳转

t0015

汇编语言最初设计时就将助记符与机器语言指令一一对应,如本例所示。1助记符转换为机器语言成为了系统程序(称为汇编程序)的工作。汇编程序最终增加了精巧的“宏扩展”功能,允许程序员为常用指令序列定义参数化缩写。然而,汇编语言和机器语言之间的对应关系仍然显而易见且明确。编程仍然是一项以机器为中心的事业:每种不同类型的计算机都必须用自己的汇编语言进行编程,程序员则以机器实际执行的指令来思考。

Assembly languages were originally designed with a one-to-one correspondence between mnemonics and machine language instructions, as shown in this example.1 Translating from mnemonics to machine language became the job of a systems program known as an assembler. Assemblers were eventually augmented with elaborate “macro expansion” facilities to permit programmers to define parameterized abbreviations for common sequences of instructions. The correspondence between assembly language and machine language remained obvious and explicit, however. Programming continued to be a machine-centered enterprise: each different kind of computer had to be programmed in its own assembly language, and programmers thought in terms of the instructions that the machine would actually execute.

随着计算机的发展和竞争性设计的出现,为每台新机器重写程序变得越来越令人沮丧。人类也越来越难以跟踪大型汇编语言程序中的大量细节。人们开始希望有一种独立于机器的语言,特别是一种数值计算(当时最常见的程序类型)可以用更接近数学公式的东西来表达的语言。这些愿望导致了 20 世纪 50 年代中期 Fortran 的原始方言的开发,这是第一个可以说是高级编程语言的语言。其他高级语言也很快出现,尤其是 Lisp 和 Algol。

As computers evolved, and as competing designs developed, it became increasingly frustrating to have to rewrite programs for every new machine. It also became increasingly difficult for human beings to keep track of the wealth of detail in large assembly language programs. People began to wish for a machine-independent language, particularly one in which numerical computations (the most common type of program in those days) could be expressed in something more closely resembling mathematical formulae. These wishes led in the mid-1950s to the development of the original dialect of Fortran, the first arguably high-level programming language. Other high-level languages soon followed, notably Lisp and Algol.

将高级语言翻译成汇编语言或机器语言是系统程序(称为编译器)的工作。2编译器比汇编程序复杂得多因为当源是高级语言时,源操作和目标操作之间的一一对应关系不再存在。 Fortran 最初流行起来很慢,因为人类程序员只要付出一些努力,几乎总是可以编写比编译器运行速度更快的汇编语言程序。然而,随着时间的推移,性能差距已经缩小,并最终逆转。硬件复杂性的增加(由于流水线、多个功能单元等)和编译器技术的不断改进导致了这样一种情况,即最先进的编译器通常会生成比人类更好的代码。即使在人类可以做得更好的情况下,计算机速度和程序大小的增加也使得节省程序员的努力变得越来越重要,不仅在程序的原始构造中,而且在后续程序维护——增强和修正。劳动力成本现在远远超过计算硬件的成本。

Translating from a high-level language to assembly or machine language is the job of a systems program known as a compiler.2 Compilers are substantially more complicated than assemblers because the one-to-one correspondence between source and target operations no longer exists when the source is a high-level language. Fortran was slow to catch on at first, because human programmers, with some effort, could almost always write assembly language programs that would run faster than what a compiler could produce. Over time, however, the performance gap has narrowed, and eventually reversed. Increases in hardware complexity (due to pipelining, multiple functional units, etc.) and continuing improvements in compiler technology have led to a situation in which a state-of-the-art compiler will usually generate better code than a human being will. Even in cases in which human beings can do better, increases in computer speed and program size have made it increasingly important to economize on programmer effort, not only in the original construction of programs, but in subsequent program maintenance—enhancement and correction. Labor costs now heavily outweigh the cost of computing hardware.

1.1 语言设计的艺术

1.1 The Art of Language Design

如今,高级编程语言有数千种,而且新的语言还在不断涌现。为什么会有这么多呢?有几种可能的答案:

Today there are thousands of high-level programming languages, and new ones continue to emerge. Why are there so many? There are several possible answers:

演化。计算机科学是一门年轻的学科;我们一直在寻找更好的方法来做事。20 世纪 60 年代末和 70 年代初,“结构化编程”发生了一场革命,Fortran、Cobol 和 Basic 3等语言的基于goto的控制流让位于while循环、case (switch)语句和类似的高级结构。20 世纪 80 年代末,Algol、Pascal 和 Ada 等语言的嵌套块结构开始让位于 Smalltalk、C++、Eiffel 等语言的面向对象结构,以及十年后的 Java 和 C#。最近,Python 和 Ruby 等脚本语言开始取代更传统的编译语言,至少在快速开发方面是如此。

Evolution. Computer science is a young discipline; we're constantly finding better ways to do things. The late 1960s and early 1970s saw a revolution in “structured programming,” in which the goto-based control flow of languages like Fortran, Cobol, and Basic3 gave way to while loops, case (switch) statements, and similar higher-level constructs. In the late 1980s the nested block structure of languages like Algol, Pascal, and Ada began to give way to the object-oriented structure of languages like Smalltalk, C++, Eiffel, and—a decade later—Java and C#. More recently, scripting languages like Python and Ruby have begun to displace more traditional compiled languages, at least for rapid development.

特殊用途。有些语言是为特定问题领域设计的。各种 Lisp 方言适合处理符号数据和复杂数据结构。Icon 和 Awk 适合处理字符串。C 适合低级系统编程。Prolog 适合推理数据之间的逻辑关系。这些语言中的每一种都可以成功地用于更广泛的任务,但重点显然在于专业性。

Special Purposes. Some languages were designed for a specific problem domain. The various Lisp dialects are good for manipulating symbolic data and complex data structures. Icon and Awk are good for manipulating character strings. C is good for low-level systems programming. Prolog is good for reasoning about logical relationships among data. Each of these languages can be used successfully for a wider range of tasks, but the emphasis is clearly on the specialty.

个人偏好。不同的人喜欢不同的东西。编程的狭隘性很大程度上只是个人品味的问题。有些人喜欢 C 的简洁性,有些人讨厌它。有些人觉得递归思考很自然,而另一些人则喜欢迭代。有些人喜欢使用指针,而另一些人则喜欢 Lisp、Java 和 ML 的隐式解引用。个人偏好的强烈和多样性使得不可能有人能开发出一种普遍接受的编程语言。

Personal Preference. Different people like different things. Much of the parochialism of programming is simply a matter of taste. Some people love the terseness of C; some hate it. Some people find it natural to think recursively; others prefer iteration. Some people like to work with pointers; others prefer the implicit dereferencing of Lisp, Java, and ML. The strength and variety of personal preference make it unlikely that anyone will ever develop a universally acceptable programming language.

当然,有些语言比其他语言更成功。在众多已设计的语言中,只有几十种被广泛使用。什么使一种语言成功?同样,有几个答案:

Of course, some languages are more successful than others. Of the many that have been designed, only a few dozen are widely used. What makes a language successful? Again there are several answers:

表达能力。人们经常听到一种语言比另一种语言更“强大”的说法,尽管从形式数学意义上讲,它们都是图灵完备— 每种语言都可以用来实现任意算法,尽管用起来有些别扭。不过,语言特性显然对程序员编写清晰、简洁、可维护的代码的能力有着巨大的影响,尤其是对于非常大的系统。例如,早期版本的 Basic 和 C++ 之间没有可比性。本书主要关注的是那些有助于表达能力的因素 — 尤其是抽象功能。

Expressive Power. One commonly hears arguments that one language is more “powerful” than another, though in a formal mathematical sense they are all Turing complete—each can be used, if awkwardly, to implement arbitrary algorithms. Still, language features clearly have a huge impact on the programmer's ability to write clear, concise, and maintainable code, especially for very large systems. There is no comparison, for example, between early versions of Basic on the one hand, and C++ on the other. The factors that contribute to expressive power—abstraction facilities in particular—are a major focus of this book.

新手易用。虽然 Basic 很容易上手,但不可否认它的成功。成功的一部分原因在于其“学习曲线”非常低。多年来,入门级编程语言课程都教授 Pascal,因为至少与其他“严肃”语言相比,它紧凑且易于学习。世纪之交后不久,Java 开始扮演类似的角色;尽管它比 Pascal 复杂得多,但比 C++ 等语言更简单。为了重新追求简单性,近年来一些入门课程已转向 Python 等脚本语言。

Ease of Use for the Novice. While it is easy to pick on Basic, one cannot deny its success. Part of that success was due to its very low “learning curve.” Pascal was taught for many years in introductory programming language courses because, at least in comparison to other “serious” languages, it was compact and easy to learn. Shortly after the turn of the century, Java came to play a similar role; though substantially more complex than Pascal, it is simpler than, say, C++. In a renewed quest for simplicity, some introductory courses in recent years have turned to scripting languages like Python.

易于实现。除了学习难度低之外,Basic 的成功还在于它能够在资源有限的微型机器上轻松实现。出于类似的原因,Forth 拥有一小群忠实的追随者。可以说,Pascal 成功最重要的一个因素是其设计者 Niklaus Wirth 开发了一种简单、可移植的语言实现,并将其免费提供给世界各地的大学(参见示例 1.15)。4 Java和 Python 的设计者采取了类似的措施,让几乎所有需要的人都可以免费使用他们的语言。

Ease of Implementation. In addition to its low learning curve, Basic was successful because it could be implemented easily on tiny machines, with limited resources. Forth had a small but dedicated following for similar reasons. Arguably the single most important factor in the success of Pascal was that its designer, Niklaus Wirth, developed a simple, portable implementation of the language, and shipped it free to universities all over the world (see Example 1.15).4 The Java and Python designers took similar steps to make their language available for free to almost anyone who wants it.

标准化。几乎每种广泛使用的语言都有一个官方的国际标准或(对于几种脚本语言而言)一个单一的规范实现;在后一种情况下,规范实现几乎总是用具有标准的语言编写的。标准化(包括语言和大量库的标准化)是确保代码跨平台可移植性的唯一真正有效的方法。Pascal 的标准相对贫乏,缺少许多程序员认为必不可少的几个功能(单独编译、字符串、静态初始化、随机访问 I/O),这至少是该语言在 20 世纪 80 年代失宠的部分原因。其中许多功能由不同的供应商以不同的方式实现。

Standardization. Almost every widely used language has an official international standard or (in the case of several scripting languages) a single canonical implementation; and in the latter case the canonical implementation is almost invariably written in a language that has a standard. Standardization—of both the language and a broad set of libraries—is the only truly effective way to ensure the portability of code across platforms. The relatively impoverished standard for Pascal, which was missing several features considered essential by many programmers (separate compilation, strings, static initialization, random-access I/O), was at least partially responsible for the language's drop from favor in the 1980s. Many of these features were implemented in different ways by different vendors.

开源。当今大多数编程语言都至少有一个开源编译器或解释器,但有些语言(尤其是 C 语言)与其他语言相比,与自由分发、同行评审、社区支持的计算联系更为密切。C 语言最初是在 20 世纪早期开发的。20 世纪 70 年代,贝尔实验室的 Dennis Ritchie 和 Ken Thompson 开发了 C 语言,5与最初的 Unix 操作系统的设计相结合。多年来,Unix 逐渐发展成为世界上最具可移植性的操作系统——学术计算机科学的首选操作系统——而 C 语言与它密切相关。随着 C 语言的标准化,该语言开始在各种各样的其他平台上使用。领先的开源操作系统 Linux 就是用 C 语言编写的。截至 2015 年 6 月,C 语言及其后代占据了各种与语言相关的在线内容的一半以上,包括网页参考、图书销售、招聘信息和开源存储库更新。

Open Source. Most programming languages today have at least one open-source compiler or interpreter, but some languages—C in particular—are much more closely associated than others with freely distributed, peer-reviewed, community-supported computing. C was originally developed in the early 1970s by Dennis Ritchie and Ken Thompson at Bell Labs,5 in conjunction with the design of the original Unix operating system. Over the years Unix evolved into the world's most portable operating system—the OS of choice for academic computer science—and C was closely associated with it. With the standardization of C, the language became available on an enormous variety of additional platforms. Linux, the leading open-source operating system, is written in C. As of June 2015, C and its descendants account for well over half of a variety oflanguage-related on-line content, including web page references, book sales, employment listings, and open-source repository updates.

优秀的编译器。Fortran 的成功很大程度上要归功于极其优秀的编译器。这在一定程度上是历史的偶然。Fortran 的存在时间比其他任何语言都要长,各大公司都投入了大量的时间和金钱来开发能够生成非常快的代码的编译器。然而,这也是一个语言设计的问题:Fortran 90 之前的 Fortran 方言缺少递归和指针,这些功能大大增加了生成快速代码的任务的复杂度(至少对于那些没有它们也能以合理的方式编写的程序而言!)。同样,一些语言(例如 Common Lisp)之所以成功,部分原因是它们的编译器和支持工具能够出色地帮助程序员管理非常大的项目。

Excellent Compilers. Fortran owes much of its success to extremely good compilers. In part this is a matter of historical accident. Fortran has been around longer than anything else, and companies have invested huge amounts of time and money in making compilers that generate very fast code. It is also a matter of language design, however: Fortran dialects prior to Fortran 90 lacked recursion and pointers, features that greatly complicate the task of generating fast code (at least for programs that can be written in a reasonable fashion without them!). In a similar vein, some languages (e.g., Common Lisp) have been successful in part because they have compilers and supporting tools that do an unusually good job of helping the programmer manage very large projects.

经济、赞助和惯性。最后,除了技术优势之外,还有其他因素对成功有很大影响。强大赞助商的支持就是其中之一。PL/I 至少在初步估计中,其生命力归功于 IBM。Cobol 和 Ada 的生命力归功于美国国防部。C# 的生命力归功于微软。近年来,Objective-C 作为 iPhone 和 iPad 应用程序的官方语言,人气飙升。在生命周期的另一端,一些语言在出现“更好”的替代方案后仍被广泛使用,因为有大量已安装的软件和程序员专业知识,而替换这些语言的成本太高。例如,世界上许多金融基础设施仍然主要在 Cobol 中运行。

Economics, Patronage, and Inertia. Finally, there are factors other than technical merit that greatly influence success. The backing of a powerful sponsor is one. PL/I, at least to first approximation, owed its life to IBM. Cobol and Ada owe their life to the U. S. Department of Defense. C# owes its life to Microsoft. In recent years, Objective-C has enjoyed an enormous surge in popularity as the official language for iPhone and iPad apps. At the other end of the life cycle, some languages remain widely used long after “better” alternatives are available, because of a huge base of installed software and programmer expertise, which would cost too much to replace. Much of the world's financial infrastructure, for example, still functions primarily in Cobol.

显然,没有哪个单一因素可以决定一种语言是否“好”。在研究编程语言时,我们需要从多个角度考虑问题。特别是,我们需要考虑程序员和语言实现者的观点。有时这些观点会一致,比如对执行速度的渴望。然而,通常会出现冲突和权衡,因为功能的概念吸引力与其实现成本相平衡。当实现不仅对使用该功能的程序施加成本,而且对不使用此功能的程序也施加成本时,权衡就变得特别棘手。

Clearly no single factor determines whether a language is “good.” As we study programming languages, we shall need to consider issues from several points of view. In particular, we shall need to consider the viewpoints of both the programmer and the language implementor. Sometimes these points of view will be in harmony, as in the desire for execution speed. Often, however, there will be conflicts and tradeoffs, as the conceptual appeal of a feature is balanced against the cost of its implementation. The tradeoff becomes particularly thorny when the implementation imposes costs not only on programs that use the feature, but also on programs that do not.

在计算发展的早期,实现者的观点占主导地位。编程语言逐渐发展成为一种告诉计算机该做什么的手段。然而,对于程序员来说,语言更恰当的定义是表达算法的一种手段。正如自然语言限制了阐述和论述一样,编程语言也限制了什么可以表达,什么不能轻易表达,并且对程序员的想法有着深刻而微妙的影响。Donald Knuth 建议将编程视为告诉另一个人你希望计算机做什么的艺术 [ Knu84 ]6。这个定义也许是最好的妥协。它承认概念清晰度和实现效率都是基本关注点。本书试图通过同时考虑其涉及的每个主题的概念和实现方面来体现这种妥协精神。

In the early days of computing the implementor's viewpoint was predominant. Programming languages evolved as a means of telling a computer what to do. For programmers, however, a language is more aptly defined as a means of expressing algorithms. Just as natural languages constrain exposition and discourse, so programming languages constrain what can and cannot easily be expressed, and have both profound and subtle influence over what the programmer can think. Donald Knuth has suggested that programming be regarded as the art of telling another human being what one wants the computer to do [Knu84].6 This definition perhaps strikes the best sort of compromise. It acknowledges that both conceptual clarity and implementation efficiency are fundamental concerns. This book attempts to capture this spirit of compromise, by simultaneously considering the conceptual and implementation aspects of each of the topics it covers.

设计与实现

Design & Implementation

1.1 简介

1.1 Introduction

本书的侧边栏将重点介绍语言设计和语言实现之间的相互作用。除其他事项外,我们将考虑

Throughout the book, sidebars like this one will highlight the interplay of language design and language implementation. Among other things, we will consider

 实现的难易程度显著影响语言的成功的情况(如本节中提到的情况)

 Cases (such as those mentioned in this section) in which ease or difficulty of implementation significantly affected the success of a language

 许多设计人员现在认为这些语言特性是错误的,至少部分原因是由于实现困难

 Language features that many designers now believe were mistakes, at least in part because of implementation difficulties

 某些语言省略了可能有用的功能,因为担心它们可能太难或太慢而无法实现

 Potentially useful features omitted from some languages because of concern that they might be too difficult or slow to implement

 至少部分地引入语言特性以促进高效或优雅的实现

 Language features introduced at least in part to facilitate efficient or elegant implementations

 机器架构导致合理特性变得不合理昂贵的情况

 Cases in which a machine architecture makes reasonable features unreasonably expensive

 实现过程中涉及的各种其他权衡

 Various other tradeoffs in which implementation plays a significant role

边栏的完整列表见附录B。

A complete list of sidebars appears in Appendix B.

1.2 编程语言范围

1.2 The Programming Language Spectrum

例 1.3

Example 1.3

编程语言的分类

Classification of programming languages

现有的许多语言可以根据其计算模型分为不同的类别。图 1.1显示了一组常见的类别。顶级划分区分了声明性语言和命令性语言,声明性语言的重点是计算机要做什么,命令性语言的重点是计算机应该如何做。

The many existing languages can be classified into families based on their model of computation. Figure 1.1 shows a common set of families. The top-level division distinguishes between the declarative languages, in which the focus is on what the computer is to do, and the imperative languages, in which the focus is on how the computer should do it.■

编号:F01-01-9780124104099
图 1.1 编程语言的分类。请注意,分类是模糊的,有待商榷。特别是,函数式语言可能是面向对象的,而许多作者并不认为函数式编程是声明式的。

声明式语言在某种意义上是“更高级的”;它们更符合程序员的观点,而不太符合实现者的观点。然而,命令式语言占主导地位,主要是出于性能原因。在声明式语言的设计中,存在着一种矛盾,一方面希望摆脱“不相关的”实现细节,另一方面又需要足够接近细节以至少控制算法的轮廓。毕竟,高效算法的设计是计算机科学的大部分内容。目前尚不清楚在多大程度上以及在哪些问题领域中,我们可以期望编译器为非常高抽象层次上陈述的问题找到好的算法。在编译器无法找到好算法的任何领域中,程序员都需要能够明确指定一个算法。

Declarative languages are in some sense “higher level”; they are more in tune with the programmer's point of view, and less with the implementor's point of view. Imperative languages predominate, however, mainly for performance reasons. There is a tension in the design of declarative languages between the desire to get away from “irrelevant” implementation details and the need to remain close enough to the details to at least control the outline of an algorithm. The design of efficient algorithms, after all, is what much of computer science is about. It is not yet clear to what extent, and in what problem domains, we can expect compilers to discover good algorithms for problems stated at a very high level of abstraction. In any domain in which the compiler cannot find a good algorithm, the programmer needs to be able to specify one explicitly.

在声明式和命令式语法家族中,有几个重要的子家族:

Within the declarative and imperative families, there are several important subfamilies:

 函数式语言采用基于函数递归定义的计算模型。它们的灵感来自lambda 演算,这是 Alonzo Church 在 20 世纪 30 年代开发的一种正式计算模型。本质上,程序被视为从输入到输出的函数,通过细化过程以更简单的函数来定义。此类别的语言包括 Lisp、ML 和 Haskell。

 Functional languages employ a computational model based on the recursive definition of functions. They take their inspiration from the lambda calculus, a formal computational model developed by Alonzo Church in the 1930s. In essence, a program is considered a function from inputs to outputs, defined in terms of simpler functions through a process of refinement. Languages in this category include Lisp, ML, and Haskell.

 数据流语言将计算建模为原始功能节点之间的信息流(令牌) 。它们提供了一种固有的并行模型:节点由输入令牌的到达触发,并且可以并发操作。Id 和 Val 是数据流语言的示例。Sisal 是 Val 的后代,通常被描述为一种函数式语言。

 Dataflow languages model computation as the flow of information (tokens) among primitive functional nodes. They provide an inherently parallel model: nodes are triggered by the arrival of input tokens, and can operate concurrently. Id and Val are examples of dataflow languages. Sisal, a descendant of Val, is more often described as a functional language.

 逻辑基于约束的语言从谓词逻辑中汲取灵感。它们将计算建模为尝试查找满足某些指定关系的值,使用目标导向搜索通过一系列逻辑规则。Prolog 是最著名的逻辑语言。该术语有时也适用于 SQL 数据库语言、XSLT 脚本语言以及电子表格(如 Excel 及其前身)的可编程方面。

 Logic or constraint-based languages take their inspiration from predicate logic. They model computation as an attempt to find values that satisfy certain specified relationships, using goal-directed search through a list of logical rules. Prolog is the best-known logic language. The term is also sometimes applied to the SQL database language, the XSLT scripting language, and programmable aspects of spreadsheets such as Excel and its predecessors.

 冯·诺依曼语言可能是最常见和应用最广泛的语言。它们包括 Fortran、Ada、C 以及所有其他以修改变量为基本计算手段的语言。7函数式语言基于具有值的表达式,而冯·诺依曼语言则基于语句(尤其是赋值语句),这些语句通过改变内存值的副作用来影响后续计算。

 The von Neumann languages are probably the most familiar and widely used. They include Fortran, Ada, C, and all of the others in which the basic means of computation is the modification of variables.7 Whereas functional languages are based on expressions that have values, von Neumann languages are based on statements (assignments in particular) that influence subsequent computation via the side effect of changing the value of memory.

 面向对象语言的根源可以追溯到 Simula 67。大多数面向对象语言与冯·诺依曼语言密切相关,但具有更结构化和分布式的内存和计算模型。面向对象语言不是将计算描绘成单片处理器对单片内存的操作,而是将其描绘成半独立对象之间的交互,每个对象都有自己的内部状态和用于管理该状态的子例程。Smalltalk 是最纯粹的面向对象语言;C++ 和 Java 可能是使用最广泛的语言。也可以设计面向对象的函数式语言(其中最著名的是 CLOS [ Kee89 ] 和 OCaml),但它们往往具有强烈的命令式风格。

 Object-oriented languages trace their roots to Simula 67. Most are closely related to the von Neumann languages, but have a much more structured and distributed model of both memory and computation. Rather than picture computation as the operation of a monolithic processor on a monolithic memory, object-oriented languages picture it as interactions among semi-independent objects, each of which has both its own internal state and subroutines to manage that state. Smalltalk is the purest of the object-oriented languages; C++ and Java are probably the most widely used. It is also possible to devise object-oriented functional languages (the best known of these are CLOS [Kee89] and OCaml), but they tend to have a strong imperative flavor.

 脚本语言的特点是,它们强调协调或“粘合”来自周围环境的组件。一些脚本语言最初是为特定目的而开发的:cshbash是作业控制 (shell) 程序的输入语言;PHP 和 JavaScript 主要用于生成动态 Web 内容;Lua 被广泛用于控制计算机游戏。其他语言,包括 Perl、Python 和 Ruby,则更注重通用性。大多数语言都强调快速原型设计,更注重表达的简易性而不是执行速度。

 Scripting languages are distinguished by their emphasis on coordinating or “gluing together” components drawn from some surrounding context. Several scripting languages were originally developed for specific purposes: csh and bash are the input languages of job control (shell) programs; PHP and JavaScript are primarily intended for the generation of dynamic web content; Lua is widely used to control computer games. Other languages, including Perl, Python, and Ruby, are more deliberately general purpose. Most place an emphasis on rapid prototyping, with a bias toward ease of expression over speed of execution.

有人可能会认为并发(并行)语言应该形成一个单独的家族(本书确实用一章来介绍此类语言),但并发和顺序执行之间的区别与上述分类基本无关。目前,大多数并发程序都是使用特殊库包或编译器结合 Fortran 或 C 等顺序语言编写的。一些广泛使用的语言(包括 Java、C# 和 Ada)具有明确的并发特性。研究人员正在研究此处提到的每个语言家族中的并发性。

One might suspect that concurrent (parallel) languages would form a separate family (and indeed this book devotes a chapter to such languages), but the distinction between concurrent and sequential execution is mostly independent of the classifications above. Most concurrent programs are currently written using special library packages or compilers in conjunction with a sequential language such as Fortran or C. A few widely used languages, including Java, C#, and Ada, have explicitly concurrent features. Researchers are investigating concurrency in each of the language families mentioned here.

例 1.4

Example 1.4

C 语言中的 GCD 函数

GCD function in C

作为语言家族对比的一个简单示例,请考虑本章开头介绍的最大公约数 (GCD) 问题。对于这个问题,选择冯·诺依曼、函数式或逻辑编程不仅会影响代码的外观,还会影响程序员的思维方式。冯·诺依曼算法版本的算法非常重要:

As a simple example of the contrast among language families, consider the greatest common divisor (GCD) problem introduced at the beginning of this chapter. The choice among, say, von Neumann, functional, or logic programming for this problem influences not only the appearance of the code, but how the programmer thinks. The von Neumann algorithm version of the algorithm is very imperative:

要计算abgcd,请检查ab是否相等。如果相等,则打印其中一个并停止。否则,用它们的差值替换较大的一个并重复。

To compute the gcd of a and b, check to see if a and b are equal. If so, print one of them and stop. Otherwise, replace the larger one by their difference and repeat.

该算法的 C 代码如图1.2所示。■

C code for this algorithm appears at the top of Figure 1.2. ■

传真:01-02-9780124104099
图 1.2 C(顶部)、OCaml(中间)和 Prolog(底部)​​中的 GCD 算法。所有三个版本都假设(不检查)它们的输入是正整数。

例 1.5

Example 1.5

OCaml 中的 GCD 函数

GCD function in OCaml

在函数式语言中,重点是输出与输入的数学关系:

In a functional language, the emphasis is on the mathematical relationship of outputs to inputs:

ab的最大公约数定义为 (1) ab相等时为a,(2) a > b时为bab最大公约数,以及 (3) b > a时为aba最大公约数。要计算给定一对数字的最大公约数,请展开并简化此定义直到终止。

The gcd of a and b is defined to be (1) a when a and b are equal, (2) the gcd of b and ab when a > b, and (3) the gcd of a and ba when b > a. To compute the gcd of a given pair of numbers, expand and simplify this definition until it terminates.

图 1.2中间显示了此算法的 OCaml 版本。关键字let引入定义;rec表示允许递归(自引用);函数的参数位于名称(在本例中为gcd)和等号之间。■

An OCaml version of this algorithm appears in the middle of Figure 1.2. The keyword let introduces a definition; rec indicates that it is permitted to be recursive (self-referential); arguments for a function come between the name (in this case, gcd) and the equals sign. ■

例 1.6

Example 1.6

Prolog中的GCD规则

GCD rules in Prolog

在逻辑语言中,程序员指定一组公理和证明规则,使系统能够找到所需的值:

In a logic language, the programmer specifies a set of axioms and proof rules that allows the system to find desired values:

如果 (1) abg都相等; (2) a 大于b并且存在一个数 c 使得 c 为 a − b 且 gcd(c, b, g) 为真;或 (3) a 小于 b 并且存在一个数c使得cb − agcd(c, a, g) 为真,则命题 gcd(a, b, g) 为计算数字gcd 请搜索一个数字g 各种数字c),这些规则允许人们证明gcd( a , b, g)为真。

The proposition gcd(a, b, g) is true if (1) a, b, and g are all equal; (2) a is greater than b and there exists a number c such that c is ab and gcd(c, b, g) is true; or (3) a is less than b and there exists a number c such that c is ba and gcd(c, a, g) is true. To compute the gcd of a given pair of numbers, search for a number g (and various numbers c) for which these rules allow one to prove that gcd(a, b, g) is true.

图 1.2底部显示了该算法的 Prolog 版本。如果将 :- 理解为“if”,将逗号理解为“and”,可能会更容易理解。■

A Prolog version of this algorithm appears at the bottom of Figure 1.2. It may be easier to understand if one reads “if” for :- and “and” for commas. ■

需要强调的是,语言家族之间的区别并不明显。例如,冯·诺依曼语言和面向对象语言之间的区别通常非常模糊,而且许多脚本语言也是面向对象的。大多数函数式和逻辑语言都包含一些命令式特性,而最近几种命令式语言也增加了函数式特性。上述描述旨在捕捉各家族的一般特征,而不是提供正式定义。

It should be emphasized that the distinctions among language families are not clear-cut. The division between the von Neumann and object-oriented languages, for example, is often very fuzzy, and many scripting languages are also object-oriented. Most of the functional and logic languages include some imperative features, and several recent imperative languages have added functional features. The descriptions above are meant to capture the general flavor of the families, without providing formal definitions.

本书主要关注命令式语言(冯·诺依曼语言和面向对象语言)。然而,许多问题都跨越了不同的范畴,感兴趣的读者会发现本书大多数章节中都有很多适用于替代计算模型的内容。第11 章第 14章包含有关函数式、逻辑、并发和脚本语言的附加材料。

Imperative languages—von Neumann and object-oriented—receive the bulk of the attention in this book. Many issues cut across family lines, however, and the interested reader will discover much that is applicable to alternative computational models in most chapters of the book. Chapters 11 through 14 contain additional material on functional, logic, concurrent, and scripting languages.

1.3 为什么要学习编程语言?

1.3 Why Study Programming Languages?

编程语言是计算机科学和典型计算机科学课程的核心。与大多数车主一样,熟悉一种或多种高级语言的学生通常对学习其他语言以及了解“引擎盖下”发生的事情感到好奇。学习语言很有趣。它也很实用。

Programming languages are central to computer science, and to the typical computer science curriculum. Like most car owners, students who have become familiar with one or more high-level languages are generally curious to learn about other languages, and to know what is going on “under the hood.” Learning about languages is interesting. It's also practical.

首先,对语言设计和实现的良好理解可以帮助人们为任何给定的任务选择最合适的语言。大多数语言在某些方面比其他方面更胜一筹。很少有程序员会选择 Fortran 进行符号计算或字符串处理,但其他选择却远没有那么明确。对于系统编程,应该选择 C、C++ 还是 C#?对于科学计算,应该选择 Fortran 还是 C?对于基于 Web 的应用程序,应该选择 PHP 还是 Ruby?对于嵌入式系统,应该选择 Ada 还是 C?对于图形用户界面,应该选择 Visual Basic 还是 Java?本书应该可以帮助您做出这样的决定。

For one thing, a good understanding of language design and implementation can help one choose the most appropriate language for any given task. Most languages are better for some things than for others. Few programmers are likely to choose Fortran for symbolic computing or string processing, but other choices are not nearly so clear-cut. Should one choose C, C++, or C# for systems programming? Fortran or C for scientific computations? PHP or Ruby for a web-based application? Ada or C for embedded systems? Visual Basic or Java for a graphical user interface? This book should help equip you to make such decisions.

同样,本书应该使学习新语言变得更容易。许多语言是密切相关的。如果你已经了解 C++,那么 Java 和 C# 就更容易学习;如果你已经了解 Scheme,那么 Common Lisp 就更容易学习;如果你已经了解 ML,那么 Haskell 就更容易学习。更重要的是,所有编程语言都有一些基本概念。这些概念中的大多数都是本书各章的主题:类型、控制(迭代、选择、递归、非确定性、并发)、抽象和命名。用这些概念来思考,可以更容易地吸收新语言的语法(形式)和语义(含义),而不是凭空而来。这种情况类似于自然界中发生的情况语言:熟悉语法形式可以更轻松地学习外语。

Similarly, this book should make it easier to learn new languages. Many languages are closely related. Java and C# are easier to learn if you already know C++; Common Lisp if you already know Scheme; Haskell if you already know ML. More importantly, there are basic concepts that underlie all programming languages. Most of these concepts are the subject of chapters in this book: types, control (iteration, selection, recursion, nondeterminacy, concurrency), abstraction, and naming. Thinking in terms of these concepts makes it easier to assimilate the syntax (form) and semantics (meaning) of new languages, compared to picking them up in a vacuum. The situation is analogous to what happens in natural languages: a good knowledge of grammatical forms makes it easier to learn a foreign language.

无论您学习什么语言,了解其设计和实现中的决策都可以帮助您更好地使用它。本书应该可以帮助您:

Whatever language you learn, understanding the decisions that went into its design and implementation will help you use it better. This book should help you:

理解晦涩难懂的功能。典型的 C++ 程序员很少使用联合、多重继承、可变数量的参数或 .* 运算符。(如果您不知道这些是什么,请不要担心!)就像它简化了新语言的吸收一样,理解基本概念可以让您在手册中查找详细信息时更容易理解这些功能。

Understand obscure features. The typical C++ programmer rarely uses unions, multiple inheritance, variable numbers of arguments, or the .* operator. (If you don't know what these are, don't worry!) Just as it simplifies the assimilation of new languages, an understanding of basic concepts makes it easier to understand these features when you look up the details in the manual.

根据对实现成本的了解,选择表达事物的备选方法。例如,在 C++ 中,程序员可能需要避免不必要的临时变量,并尽可能使用复制构造函数,以尽量减少初始化成本。在 Java 中,他们可能希望使用Executor对象,而不是显式创建线程。对于某些(较差的)编译器,他们可能需要采用特殊的编程习惯用法来获得最快的代码:用于数组遍历的指针;x*x 而不是 x**2。在任何语言中,他们都需要能够评估抽象的备选实现之间的权衡——例如,对于位集基数等函数,在计算和表查找之间进行权衡,这些函数可以用任何一种方式实现。

Choose among alternative ways to express things, based on a knowledge of implementation costs. In C++, for example, programmers may need to avoid unnecessary temporary variables, and use copy constructors whenever possible, to minimize the cost of initialization. In Java they may wish to use Executor objects rather than explicit thread creation. With certain (poor) compilers, they may need to adopt special programming idioms to get the fastest code: pointers for array traversal; x*x instead of x**2. In any language, they need to be able to evaluate the tradeoffs among alternative implementations of abstractions—for example between computation and table lookup for functions like bit set cardinality, which can be implemented either way.

充分利用调试器、汇编器、链接器和相关工具。一般来说,高级语言程序员不需要为实现细节而烦恼。但有时,了解这些细节几乎是必不可少的。如果一个人愿意深入了解这些细节,那么顽固的错误或不寻常的系统构建问题可能会变得容易得多。

Make good use of debuggers, assemblers, linkers, and related tools. In general, the high-level language programmer should not need to bother with implementation details. There are times, however, when an understanding of those details is virtually essential. The tenacious bug or unusual system-building problem may be dramatically easier to handle if one is willing to peek at the bits.

模拟缺少有用功能的语言。某些非常有用的功能在较旧的语言中缺失,但可以通过遵循刻意的(如果不是强制的)编程风格来模拟。例如,在 Fortran 的较旧方言中,熟悉现代控制结构的程序员可以使用注释和自律来编写结构良好的代码。同样,在抽象功能较差的语言中,注释和命名约定可以帮助模仿模块化结构,并且可以使用子例程和静态变量来模仿Clu、C#、Python 和 Ruby 中极其有用的迭代器(我们将在第 6.5.3 节中学习)。

Simulate useful features in languages that lack them. Certain very useful features are missing in older languages, but can be emulated by following a deliberate (if unenforced) programming style. In older dialects of Fortran, for example, programmers familiar with modern control constructs can use comments and self-discipline to write well-structured code. Similarly, in languages with poor abstraction facilities, comments and naming conventions can help imitate modular structure, and the extremely useful iterators of Clu, C#, Python, and Ruby (which we will study in Section 6.5.3) can be imitated with subroutines and static variables.

无论语言技术出现在哪里,都要更好地利用它。大多数程序员永远不会设计或实现传统的编程语言,但大多数程序员需要语言技术来完成其他编程任务。典型的个人计算机包含数十种结构化格式的文件,包括文字处理、电子表格、演示文稿、光栅和矢量图形、音乐、视频、数据库以及各种其他应用领域。Web 内容越来越多地以 XML 表示,这是一种基于文本的格式,旨在通过 XSLT 脚本语言轻松操作(在C-14.3.5 节中讨论)。要解析、分析、生成、优化和其他操作的代码因此,几乎任何复杂的程序都可以找到操纵结构化数据的方法,所有这些代码都基于语言技术。掌握了这项技术的程序员将更有能力编写结构良好、可维护的工具。

Make better use of language technology wherever it appears. Most programmers will never design or implement a conventional programming language, but most will need language technology for other programming tasks. The typical personal computer contains files in dozens of structured formats, encompassing word processing, spreadsheets, presentations, raster and vector graphics, music, video, databases, and a wide variety of other application domains. Web content is increasingly represented in XML, a text-based format designed for easy manipulation in the XSLT scripting language (discussed in Section C-14.3.5). Code to parse, analyze, generate, optimize, and otherwise manipulate structured data can thus be found in almost any sophisticated program, and all of this code is based on language technology. Programmers with a strong grasp of this technology will be in a better position to write well-structured, maintainable tools.

类似地,大多数工具本身可以通过启动配置文件、命令行参数、输入命令或内置扩展语言(将在第 14 章中详细讨论)进行定制。我的主目录中有 250 多个单独的配置(“首选项”)文件。我个人的emacs文本编辑器配置文件包含 1200 多行 Lisp 代码。如今,几乎任何复杂程序的用户都需要充分利用配置或扩展语言。这种程序的设计者要么需要采用(并调整)某些现有的扩展语言,要么发明自己的新符号。精通语言理论的程序员将能够更好地设计出优雅、结构良好的符号,以满足当前用户的需求并促进未来的开发。

In a similar vein, most tools themselves can be customized, via start-up configuration files, command-line arguments, input commands, or built-in extension languages (considered in more detail in Chapter 14). My home directory holds more than 250 separate configuration (“preference”) files. My personal configuration files for the emacs text editor comprise more than 1200 lines of Lisp code. The user of almost any sophisticated program today will need to make good use of configuration or extension languages. The designers of such a program will need either to adopt (and adapt) some existing extension language, or to invent new notation of their own. Programmers with a strong grasp of language theory will be in a better position to design elegant, well-structured notation that meets the needs of current users and facilitates future development.

最后,如果您愿意,本书应该可以帮助您为进一步研究语言设计或实现做好准备。如果您对这些领域感兴趣,它还将帮助您理解语言与操作系统和体系结构之间的交互。

Finally, this book should help prepare you for further study in language design or implementation, should you be so inclined. It will also equip you to understand the interactions of languages with operating systems and architectures, should those areas draw your interest.

01-01-9780124104099检查你的理解

Check Your Understanding

1. 机器语言和汇编语言有什么区别?

1. What is the difference between machine language and assembly language?

2. 高级语言在哪些方面比汇编语言有所改进?在哪些情况下使用汇编语言编程仍然有意义?

2. In what way(s) are high-level languages an improvement on assembly language? Are there circumstances in which it still make sense to program in assembler?

3. 为什么有这么多编程语言?

3. Why are there so many programming languages?

4. 什么使得一种编程语言成功?

4. What makes a programming language successful?

5. 列举下列类别中的三种语言:冯·诺依曼语言、函数式语言、面向对象语言。列举两种逻辑语言。列举两种广泛使用的并发语言。

5. Name three languages in each of the following categories: von Neumann, functional, object-oriented. Name two logic languages. Name two widely used concurrent languages.

6. 声明式语言与命令式语言有何区别?

6. What distinguishes declarative languages from imperative languages?

7. 哪个组织率先开发了 Ada?

7. What organization spearheaded the development of Ada?

8. 通常认为第一种高级编程语言是什么?

8. What is generally considered the first high-level programming language?

9. 第一个函数式语言是什么?

9. What was the first functional language?

10.为什么 图 1.1中没有将并发语言列为单独的语言家族?

10. Why aren't concurrent languages listed as a separate family in Figure 1.1?

1.4 编译和解释

1.4 Compilation and Interpretation

例 1.7

Example 1.7

纯编译

Pure compilation

在最高的抽象层次上,高级语言中程序的编译和执行看起来是这样的:

At the highest level of abstraction, the compilation and execution of a program in a high-level language look something like this:

u01-01-9780124104099

编译器将高级源程序翻译成等效的目标程序(通常是机器语言),然后就消失了。在稍后的某个时间,用户告诉操作系统运行目标程序。编译器是编译期间的控制点;目标程序是其自身执行期间的控制点。编译器本身是一个机器语言程序,大概是通过编译其他高级程序创建的。当以操作系统理解的格式写入文件时,机器语言通常称为目标代码。■

The compiler translates the high-level source program into an equivalent target program (typically in machine language), and then goes away. At some arbitrary later time, the user tells the operating system to run the target program. The compiler is the locus of control during compilation; the target program is the locus of control during its own execution. The compiler is itself a machine language program, presumably created by compiling some other high-level program. When written to a file in a format understood by the operating system, machine language is commonly known as object code. ■

例 1.8

Example 1.8

纯粹的解释

Pure interpretation

高级语言的另一种实现方式称为解释

An alternative style of implementation for high-level languages is known as interpretation:

u01-02-9780124104099

与编译器不同,解释器在应用程序执行期间一直存在。事实上,解释器是执行期间的控制中心。实际上,解释器实现了一个虚拟机,其“机器语言”是高级编程语言。解释器一次或多或少读取该语言的语句,并在执行过程中执行它们。■

Unlike a compiler, an interpreter stays around for the execution of the application. In fact, the interpreter is the locus of control during that execution. In effect, the interpreter implements a virtual machine whose “machine language” is the high-level programming language. The interpreter reads statements in that language more or less one at a time, executing them as it goes along. ■

一般而言,解释比编译具有更大的灵活性和更好的诊断(错误消息)。由于直接执行源代码,解释器可以包含出色的源代码级调试器。它还可以处理那些程序基本特性(例如变量的大小和类型,甚至哪些名称引用哪些变量)取决于输入数据的语言。某些语言特性几乎不可能在没有解释的情况下实现:例如,在 Lisp 和 Prolog 中,程序可以编写自己的新片段并动态执行它们。(几种脚本语言也提供此功能。)将程序实现的决策推迟到运行时称为后期绑定;我们将在第 3.1 节中详细讨论它。

In general, interpretation leads to greater flexibility and better diagnostics (error messages) than does compilation. Because the source code is being executed directly, the interpreter can include an excellent source-level debugger. It can also cope with languages in which fundamental characteristics of the program, such as the sizes and types of variables, or even which names refer to which variables, can depend on the input data. Some language features are almost impossible to implement without interpretation: in Lisp and Prolog, for example, a program can write new pieces of itself and execute them on the fly. (Several scripting languages also provide this capability.) Delaying decisions about program implementation until run time is known as late binding; we will discuss it at greater length in Section 3.1.

相比之下,编译通常会带来更好的性能。通常,编译时做出的决定是运行时不需要做出的决定。例如,如果编译器可以保证变量x始终位于位置49378,它就可以生成机器语言指令,以便在源程序引用x时访问此位置。相比之下,解释器可能需要在每次访问x时在表中查找它,以找到它的位置。由于程序(的最终版本)仅编译一次,但通常执行多次,因此节省的成本可能非常可观,特别是如果解释器在循环的每次迭代中都做不必要的工作。

Compilation, by contrast, generally leads to better performance. In general, a decision made at compile time is a decision that does not need to be made at run time. For example, if the compiler can guarantee that variable x will always lie at location 49378, it can generate machine language instructions that access this location whenever the source program refers to x. By contrast, an interpreter may need to look x up in a table every time it is accessed, in order to find its location. Since the (final version of a) program is compiled only once, but generally executed many times, the savings can be substantial, particularly if the interpreter is doing unnecessary work in every iteration of a loop.

例 1.9

Example 1.9

混合编译和解释

Mixing compilation and interpretation

虽然编译和解释之间的概念差异很明显,但大多数语言实现都包含两者的混合。它们通常看起来像这样:

While the conceptual difference between compilation and interpretation is clear, most language implementations include a mixture of both. They typically look like this:

u01-03-9780124104099

如果初始翻译器很简单,我们通常说一种语言是“解释型”的。如果翻译器很复杂,我们说这种语言是“编译型”的。这种区别可能令人困惑,因为“简单”和“复杂”是主观术语,并且编译器(复杂翻译器)可以生成代码,然后由复杂的虚拟机(解释器)执行;事实上,这正是 Java 中默认发生的情况。如果翻译器彻底分析了语言(而不是进行某种“机械”转换),并且中间程序与源程序没有太大相似之处,我们仍然说这种语言是编译型的。这两个特征——彻底的分析和非平凡的转换——是编译的标志。■

We generally say that a language is “interpreted” when the initial translator is simple. If the translator is complicated, we say that the language is “compiled.” The distinction can be confusing because “simple” and “complicated” are subjective terms, and because it is possible for a compiler (complicated translator) to produce code that is then executed by a complicated virtual machine (interpreter); this is in fact precisely what happens by default in Java. We still say that a language is compiled if the translator analyzes it thoroughly (rather than effecting some “mechanical” transformation), and if the intermediate program does not bear a strong resemblance to the source. These two characteristics—thorough analysis and nontrivial transformation—are the hallmarks of compilation. ■

设计与实现

Design & Implementation

1.2 编译型语言和解释型语言

1.2 Compiled and interpreted languages

某些语言(例如 Smalltalk 和 Python)有时被称为“解释型语言”,因为它们的大多数语义错误检查必须在运行时执行。某些其他语言(例如 Fortran 和 C)有时被称为“编译型语言”,因为它们的几乎所有语义错误检查都可以静态执行。这个术语并不完全正确:C 和 Fortran 的解释器可以轻松构建,编译器可以生成代码来执行最广泛的动态语义检查。话虽如此,语言设计对“可编译性”有着深远的影响。

Certain languages (e.g., Smalltalk and Python) are sometimes referred to as “interpreted languages” because most of their semantic error checking must be performed at run time. Certain other languages (e.g., Fortran and C) are sometimes referred to as “compiled languages” because almost all of their semantic error checking can be performed statically. This terminology isn't strictly correct: interpreters for C and Fortran can be built easily, and a compiler can generate code to perform even the most extensive dynamic semantic checks. That said, language design has a profound effect on “compilability.”

在实践中,我们看到了广泛的实现策略:

In practice one sees a broad spectrum of implementation strategies:

例 1.10

Example 1.10

预处理

Preprocessing

 大多数解释型语言都使用初始翻译器(预处理器),它会删除注释和空格,并将字符组合成标记,例如关键字、标识符、数字和符号。翻译器还可以以宏汇编程序的样式扩展缩写。最后,它可以识别更高级的语法结构,例如循环和子程序。目标是生成一个中间形式,该形式反映了源的结构,但可以更有效地进行解释。■

 Most interpreted languages employ an initial translator (a preprocessor) that removes comments and white space, and groups characters together into tokens such as keywords, identifiers, numbers, and symbols. The translator may also expand abbreviations in the style of a macro assembler. Finally, it may identify higher-level syntactic structures, such as loops and subroutines. The goal is to produce an intermediate form that mirrors the structure of the source, but can be interpreted more efficiently. ■

在 Basic 的一些早期实现中,手册实际上建议从程序中删除注释以提高其性能。这些实现是纯粹的解释器;它们每次执行程序的给定部分时都会重新读取(然后忽略)注释。它们没有初始翻译器。

In some very early implementations of Basic, the manual actually suggested removing comments from a program in order to improve its performance. These implementations were pure interpreters; they would re-read (and then ignore) the comments every time they executed a given part of the program. They had no initial translator.

例 1.11

Example 1.11

库例程和链接

Library routines and linking

 典型的 Fortran 实现接近于纯编译。编译器将 Fortran 源代码翻译成机器语言。但是,它通常依赖于不属于原始程序的子例程库的存在。示例包括数学函数( sincoslog等)和 I/O。编译器依赖于一个单独的程序(称为链接器)将适当的库例程合并到最终程序中:

 The typical Fortran implementation comes close to pure compilation. The compiler translates Fortran source into machine language. Usually, however, it counts on the existence of a library of subroutines that are not part of the original program. Examples include mathematical functions (sin, cos, log, etc.) and I/O. The compiler relies on a separate program, known as a linker, to merge the appropriate library routines into the final program:

u01-04-9780124104099

从某种意义上说,人们可以将库例程视为硬件指令集的扩展。然后,可以将编译器视为为虚拟机生成代码,该代码同时包含硬件和库的功能。

In some sense, one may think of the library routines as extensions to the hardware instruction set. The compiler can then be thought of as generating code for a virtual machine that includes the capabilities of both the hardware and the library.

从更字面意义上讲,可以在 Fortran 例程中找到格式化输出的解释。Fortran 允许使用格式语句来控制输出在列中的对齐方式、浮点数的有效位数和科学计数法类型、前导零的包含/隐藏等等。程序可以动态计算自己的格式。输出库例程包括一个格式解释器。在C 及其后代的printf例程中可以找到类似的解释器。■

In a more literal sense, one can find interpretation in the Fortran routines for formatted output. Fortran permits the use of format statements that control the alignment of output in columns, the number of significant digits and type of scientific notation for floating-point numbers, inclusion/suppression of leading zeros, and so on. Programs can compute their own formats on the fly. The output library routines include a format interpreter. A similar interpreter can be found in the printf routine of C and its descendants. ■

例 1.12

Example 1.12

编译后汇编

Post-compilation assembly

 许多编译器生成汇编语言而不是机器语言。这种约定有利于调试,因为汇编语言更易于人们阅读,并且使编译器不受操作系统新版本可能要求的机器语言文件格式更改的影响(只有汇编程序必须更改,许多编译器都共享它):

 Many compilers generate assembly language instead of machine language. This convention facilitates debugging, since assembly language is easier for people to read, and isolates the compiler from changes in the format of machine language files that may be mandated by new releases of the operating system (only the assembler must be changed, and it is shared by many compilers):

u01-05-9780124104099

例 1.13

Example 1.13

C 预处理器

The C preprocessor

C 语言(以及在 Unix 下运行的许多其他语言)的编译器都以预处理器开始,该预处理器可删除注释并扩展宏。预处理器还可以被指示删除代码本身的某些部分,从而提供条件编译功能,允许从同一源构建程序的多个版本:

Compilers for C (and for many other languages running under Unix) begin with a preprocessor that removes comments and expands macros. The preprocessor can also be instructed to delete portions of the code itself, providing a conditional compilation facility that allows several versions of a program to be built from the same source:

u01-06-9780124104099

例 1.14

Example 1.14

源到源翻译

Source-to-source translation

 数量惊人的编译器会以某种高级语言生成输出 — 通常是 C 语言或输入语言的某种简化版本。这种源到源的翻译在研究语言和语言开发的早期阶段尤为常见。一个著名的例子是 AT&T 最初的 C++ 编译器。这确实是一个真正的编译器,尽管它生成的是 C 而不是汇编程序:它对 C++ 源程序的语法和语义进行了完整的分析,除了极少数例外生成了程序员在运行程序之前会看到的所有错误消息。事实上,程序员通常不知道 C 编译器在后台运行。C++ 编译器不会调用 C 编译器,除非它生成的 C 代码能够通过第二轮编译而不会产生任何错误消息:

 A surprising number of compilers generate output in some high-level language—commonly C or some simplified version of the input language. Such source-to-source translation is particularly common in research languages and during the early stages of language development. One famous example was AT&T's original compiler for C++. This was indeed a true compiler, though it generated C instead of assembler: it performed a complete analysis of the syntax and semantics of the C++ source program, and with very few exceptions generated all of the error messages that a programmer would see prior to running the program. In fact, programmers were generally unaware that the C compiler was being used behind the scenes. The C++ compiler did not invoke the C compiler unless it had generated C code that would pass through the second round of compilation without producing any error messages:

u01-07-9780124104099

有时,人们会听到 C++ 编译器被称为预处理器,大概是因为它生成的高级输出会被编译。我认为这是对术语的误用:编译器试图“理解”其源代码;而预处理器则不会。预处理器根据简单的模式匹配执行转换,并且可能会生成输出,这些输出在后续转换阶段运行时会生成错误消息。

Occasionally one would hear the C++ compiler referred to as a preprocessor, presumably because it generated high-level output that was in turn compiled. I consider this a misuse of the term: compilers attempt to “understand” their source; preprocessors do not. Preprocessors perform transformations based on simple pattern matching, and may well produce output that will generate error messages when run through a subsequent stage of translation.

例 1.15

Example 1.15

引导

Bootstrapping

 许多编译器都是自托管的:它们用编译的语言编写 — Ada 编译器用 Ada 编写,C 编译器用 C 编写。这引出了一个显而易见的问题:首先如何编译编译器?答案是使用一种称为bootstrapping的技术,这个术语源于一个故意荒谬的概念,即通过拉动引导装置将自己抬离地面。简而言之,从一个简单的实现(通常是解释器)开始,并使用它来构建逐渐更复杂的版本。我们可以用一个历史例子来说明这个想法。

 Many compilers are self-hosting: they are written in the language they compile—Ada compilers in Ada, C compilers in C. This raises an obvious question: how does one compile the compiler in the first place? The answer is to use a technique known as bootstrapping, a term derived from the intentionally ridiculous notion of lifting oneself off the ground by pulling on one's bootstraps. In a nutshell, one starts with a simple implementation—often an interpreter—and uses it to build progressively more sophisticated versions. We can illustrate the idea with an historical example.

许多早期的 Pascal 编译器都是围绕 Niklaus Wirth 分发的一组工具构建的。其中包括:

Many early Pascal compilers were built around a set of tools distributed by Niklaus Wirth. These included the following:

 用 Pascal 编写的 Pascal 编译器,可以生成P 代码输出,P 代码是一种基于堆栈的语言,类似于现代 Java 编译器的字节码

 A Pascal compiler, written in Pascal, that would generate output in P-code, a stack-based language similar to the bytecode of modern Java compilers

 同一个编译器,已经翻译成 P 代码

 The same compiler, already translated into P-code

 用 Pascal 编写的 P 代码解释器

 A P-code interpreter, written in Pascal

要在本地机器上启动并运行 Pascal,该工具集的用户只需将 P 代码解释器(手动)翻译成某种本地可用的语言。这种翻译并不困难;解释器很小。通过在 P 代码上运行编译器的 P 代码版本解释器,然后可以将任意 Pascal 程序编译为 P 代码,然后可以在解释器上运行。为了获得更快的实现,可以修改 Pascal 编译器的 Pascal 版本以生成本地可用的汇编语言或机器语言,而不是生成 P 代码(这是一项稍微困难的任务)。然后可以引导此编译器(通过自身运行)以生成编译器的机器代码版本:

To get Pascal up and running on a local machine, the user of the tool set needed only to translate the P-code interpreter (by hand) into some locally available language. This translation was not a difficult task; the interpreter was small. By running the P-code version of the compiler on top of the P-code interpreter, one could then compile arbitrary Pascal programs into P-code, which could in turn be run on the interpreter. To get a faster implementation, one could modify the Pascal version of the Pascal compiler to generate a locally available variety of assembly or machine language, instead of generating P-code (a somewhat more difficult task). This compiler could then be bootstrapped—run through itself—to yield a machine-code version of the compiler:

u01-08-9780124104099

在更一般的背景下,假设我们正在为一种新的编程语言构建第一个编译器。假设我们的目标系统上有一个 C 编译器,我们可能会先用 C 的简单子集编写一个编译器,用于我们新编程语言的一个同样简单的子集。一旦这个编译器开始工作,我们就可以手动将 C 代码翻译成我们新语言的(子集),然后通过编译器本身运行新的源代码。之后,我们可以反复扩展编译器以接受更大的子集新的编程语言,再次引导它,并使用扩展语言来实现更大的子集。这种“自托管”实现实际上相当常见。■

In a more general context, suppose we were building one of the first compilers for a new programming language. Assuming we have a C compiler on our target system, we might start by writing, in a simple subset of C, a compiler for an equally simple subset of our new programming language. Once this compiler was working, we could hand-translate the C code into (the subset of) our new language, and then run the new source through the compiler itself. After that, we could repeatedly extend the compiler to accept a larger subset of the new programming language, bootstrap it again, and use the extended language to implement an even larger subset. “Self-hosting” implementations of this sort are actually quite common. ■

设计与实现

Design & Implementation

1.3 Pascal 的早期成功

1.3 The early success of Pascal

基于 P 代码的 Pascal 实现及其对引导的使用,在很大程度上促成了该语言在 20 世纪 70 年代学术界的显著成功。当时没有一个单一的硬件平台或操作系统像今天的 x86、Linux 和 Windows 那样主宰计算机领域。8 Wirth的工具包使得在一周左右的时间内几乎可以在任何平台上启动并运行 Pascal 实现。这是系统可移植性方面取得的首批重大成功之一。

The P-code-based implementation of Pascal, and its use of bootstrapping, are largely responsible for the language's remarkable success in academic circles in the 1970s. No single hardware platform or operating system of that era dominated the computer landscape the way the x86, Linux, and Windows do today.8 Wirth's toolkit made it possible to get an implementation of Pascal up and running on almost any platform in a week or so. It was one of the first great successes in system portability.

例 1.16

Example 1.16

编译解释型语言

Compiling interpreted languages

 有时会发现一些语言(例如 Lisp、Prolog、Smalltalk)的编译器允许大量后期绑定,并且传统上是解释型的。一般情况下,这些编译器必须准备好生成执行解释器大部分工作的代码,或者调用执行该工作的库。然而,在重要的特殊情况下,编译器可以生成对决策做出合理假设的代码,这些假设直到运行时才会最终确定。如果这些假设被证明是有效的,代码将运行得非常快。如果假设不正确,动态检查将发现不一致性,并恢复到解释器。■

 One will sometimes find compilers for languages (e.g., Lisp, Prolog, Smalltalk) that permit a lot of late binding, and are traditionally interpreted. These compilers must be prepared, in the general case, to generate code that performs much of the work of an interpreter, or that makes calls into a library that does that work instead. In important special cases, however, the compiler can generate code that makes reasonable assumptions about decisions that won't be finalized until run time. If these assumptions prove to be valid the code will run very fast. If the assumptions are not correct, a dynamic check will discover the inconsistency, and revert to the interpreter. ■

例 1.17

Example 1.17

动态和即时编译

Dynamic and just-in-time compilation

 在某些情况下,编程系统可能会故意将编译延迟到最后一刻。一个例子出现在语言实现(例如,Lisp 或 Prolog)中,这些实现会动态调用编译器,将新创建的源代码转换为机器语言,或针对特定输入集优化代码。另一个例子出现在 Java 实现中。Java 语言定义定义了一种与机器无关的中间形式,称为 Java字节码。字节码是 Java 程序分发的标准格式;它允许程序通过互联网轻松传输,然后在任何平台上运行。第一个 Java 实现基于字节码解释器,但现代实现使用即时编译器获得了明显更好的性能,该编译器在每次执行程序之前立即将字节码转换为机器语言:

 In some cases a programming system may deliberately delay compilation until the last possible moment. One example occurs in language implementations (e.g., for Lisp or Prolog) that invoke the compiler on the fly, to translate newly created source into machine language, or to optimize the code for a particular input set. Another example occurs in implementations of Java. The Java language definition defines a machine-independent intermediate form known as Java bytecode. Bytecode is the standard format for distribution of Java programs; it allows programs to be transferred easily over the Internet, and then run on any platform. The first Java implementations were based on byte-code interpreters, but modern implementations obtain significantly better performance with a just-in-time compiler that translates bytecode into machine language immediately before each execution of the program:

u01-09-9780124104099

同样,C# 也用于即时翻译。主 C# 编译器生成通用中间语言(CIL),然后在执行之前立即将其翻译成机器语言。CIL 刻意与语言无关,因此可以用于由各种前端编译器生成的代码。我们将在第 16.1 节详细探讨 Java 和 C# 实现。■

C#, similarly, is intended for just-in-time translation. The main C# compiler produces Common Intermediate Language (CIL), which is then translated into machine language immediately prior to execution. CIL is deliberately language independent, so it can be used for code produced by a variety of front-end compilers. We will explore the Java and C# implementations in detail in Section 16.1. ■

例 1.18

Example 1.18

微码(固件)

Microcode (firmware)

 在某些机器上(尤其是 20 世纪 80 年代中期之前设计的机器),汇编级指令集实际上并未在硬件中实现,而是在解释器上运行。解释器以称为微代码(或固件)的低级指令编写,存储在只读存储器中并由硬件执行。微代码和微编程将在 C-5.4.1 节中进一步讨论。■

 On some machines (particularly those designed before the mid-1980s), the assembly-level instruction set is not actually implemented in hardware, but in fact runs on an interpreter. The interpreter is written in low-level instructions called microcode (or firmware), which is stored in read-only memory and executed by the hardware. Microcode and microprogramming are considered further in Section C-5.4.1. ■

正如这些例子中所表明的,编译器不一定会将高级编程语言翻译成机器语言。事实上,有些编译器接受的输入我们可能根本不会立即认为是程序。例如,像 T E X 这样的文本格式化程序会将高级文档描述编译成激光打印机或照排机的命令。(许多激光打印机本身包含预安装的 Postscript 页面描述语言解释器。)数据库系统的查询语言处理器会将 SQL 等语言翻译成对文件的原始操作。甚至还有编译器将逻辑级电路规范翻译成计算机芯片的照相掩模版。虽然本书的重点是命令式编程语言,但“编译”一词适用于当我们在充分分析输入含义的情况下从一种非平凡语言自动翻译成另一种语言时。

As some of these examples make clear, a compiler does not necessarily translate from a high-level programming language into machine language. Some compilers, in fact, accept inputs that we might not immediately think of as programs at all. Text formatters like TEX, for example, compile high-level document descriptions into commands for a laser printer or phototypesetter. (Many laser printers themselves contain pre-installed interpreters for the Postscript page-description language.) Query language processors for database systems translate languages like SQL into primitive operations on files. There are even compilers that translate logic-level circuit specifications into photographic masks for computer chips. Though the focus in this book is on imperative programming languages, the term “compilation” applies whenever we translate automatically from one nontrivial language to another, with full analysis of the meaning of the input.

1.5 编程环境

1.5 Programming Environments

编译器和解释器并不是孤立存在的。程序员可以通过许多其他工具来协助他们的工作。前面提到了汇编器、调试器、预处理器和链接器。每个程序员都熟悉编辑器。它们可能配备了交叉引用功能,使程序员能够在给定对象使用点的情况下找到定义该对象的点。漂亮的打印机有助于强制执行格式约定。样式检查器强制执行的句法或语义约定可能比编译器强制执行的约定更严格(参见探索 1.14)。配置管理工具有助于跟踪大型软件系统中单独编译的模块(的许多版本)之间的依赖关系。阅读工具不仅适用于文本,也适用于可能以二进制形式存储的中间语言。分析器和其他性能分析工具通常与调试器协同工作,以帮助识别程序中消耗大量计算时间的部分。

Compilers and interpreters do not exist in isolation. Programmers are assisted in their work by a host of other tools. Assemblers, debuggers, preprocessors, and linkers were mentioned earlier. Editors are familiar to every programmer. They may be augmented with cross-referencing facilities that allow the programmer to find the point at which an object is defined, given a point at which it is used. Pretty printers help enforce formatting conventions. Style checkers enforce syntactic or semantic conventions that may be tighter than those enforced by the compiler (see Exploration 1.14). Configuration management tools help keep track of dependences among the (many versions of) separately compiled modules in a large software system. Perusal tools exist not only for text but also for intermediate languages that may be stored in binary. Profilers and other performance analysis tools often work in conjunction with debuggers to help identify the pieces of a program that consume the bulk of its computation time.

在较旧的编程环境中,工具可能会根据用户的明确请求单独执行。例如,如果正在运行的程序因“总线错误”(无效地址)消息而异常终止,则用户可以选择调用调试器来检查操作系统转储的“核心”文件。然后,他或她可能会尝试通过设置断点、启用跟踪等来识别程序错误,并在调试器的控制下再次运行该程序。一旦发现错误,用户将调用编辑器进行适当的更改。然后,他或她将重新编译修改后的程序,可能借助配置管理器。

In older programming environments, tools may be executed individually, at the explicit request of the user. If a running program terminates abnormally with a “bus error” (invalid address) message, for example, the user may choose to invoke a debugger to examine the “core” file dumped by the operating system. He or she may then attempt to identify the program bug by setting breakpoints, enabling tracing and so on, and running the program again under the control of the debugger. Once the bug is found, the user will invoke the editor to make an appropriate change. He or she will then recompile the modified program, possibly with the help of a configuration manager.

现代环境提供了更多集成工具。当集成开发环境 (IDE) 中出现无效地址错误时,用户的屏幕上可能会出现一个新窗口,其中突出显示发生错误的源代码行。然后可以在此窗口中设置断点和跟踪,而无需明确调用调试器。无需明确调用编辑器即可对源代码进行更改。如果用户在进行更改后要求重新运行程序,则可以构建新版本,而无需明确调用编译器或配置管理器。

Modern environments provide more integrated tools. When an invalid address error occurs in an integrated development environment (IDE), a new window is likely to appear on the user's screen, with the line of source code at which the error occurred highlighted. Breakpoints and tracing can then be set in this window without explicitly invoking a debugger. Changes to the source can be made without explicitly invoking an editor. If the user asks to rerun the program after making changes, a new version may be built without explicitly invoking the compiler or configuration manager.

IDE 的编辑器可能包含语言语法知识,为所有标准控制结构提供模板,并在输入时检查语法。在内部,IDE 可能不仅维护程序的源代码和目标代码,还维护部分编译的内部表示。编辑源代码时,内部表示将自动更新 - 通常是增量更新(无需重新解析源代码的大部分内容)。在某些情况下,对程序的结构更改可能首先在内部表示中实现,然后自动反映在源代码中。

The editor for an IDE may incorporate knowledge of language syntax, providing templates for all the standard control structures, and checking syntax as it is typed in. Internally, the IDE is likely to maintain not only a program's source and object code, but also a partially compiled internal representation. When the source is edited, the internal representation will be updated automatically—often incrementally (without reparsing large portions of the source). In some cases, structural changes to the program may be implemented first in the internal representation, and then automatically reflected in the source.

IDE 是 Smalltalk 的基础——几乎不可能将该语言与其图形环境分开——自 20 世纪 80 年代以来,它一直被常规用于 Common Lisp。随着图形界面的普及,集成环境已在很大程度上取代了许多语言和系统的命令行工具。流行的开源 IDE 包括 Eclipse 和 NetBeans。商业系统包括 Microsoft 的 Visual Studio 环境和 Apple 的 XCode 环境。集成的大部分外观也可以在复杂的编辑器(如emacs )中实现。

IDEs are fundamental to Smalltalk—it is nearly impossible to separate the language from its graphical environment—and have been routinely used for Common Lisp since the 1980s. With the ubiquity of graphical interfaces, integrated environments have largely displaced command-line tools for many languages and systems. Popular open-source IDEs include Eclipse and NetBeans. Commercial systems include the Visual Studio environment from Microsoft and the XCode environment from Apple. Much of the appearance of integration can also be achieved within sophisticated editors such as emacs.

01-01-9780124104099检查你的理解

Check Your Understanding

11. 解释解释和编译的区别。这两种方法各自的优缺点是什么?

11. Explain the distinction between interpretation and compilation. What are the comparative advantages and disadvantages of the two approaches?

12.  Java 是编译型还是解释型(或两者皆有)?你怎么知道?

12. Is Java compiled or interpreted (or both)? How do you know?

13. 编译器和预处理器有什么区别?

13. What is the difference between a compiler and a preprocessor?

14. 最初的 AT&T C++ 编译器采用的中间形式是什么?

14. What was the intermediate form employed by the original AT&T C++ compiler?

15. 什么是P码?

15. What is P-code?

16. 什么是引导?

16. What is bootstrapping?

17. 什么是即时编译器?

17. What is a just-in-time compiler?

18. 说出两种程序可以“动态”编写新代码的语言。

18. Name two languages in which a program can write new pieces of itself “on the fly.”

19. 简要描述三种“非常规”编译器——其目的不是准备在通用处理器上执行的高级程序。

19. Briefly describe three “unconventional” compilers—compilers whose purpose is not to prepare a high-level program for execution on a general-purpose processor.

20. 列出六种在更大的编程环境中通常支持编译器工作的工具。

20. List six kinds of tools that commonly support the work of a compiler within a larger programming environment.

21. 解释集成开发环境 (IDE) 与命令行工具集合有何不同。

21. Explain how an integrated development environment (IDE) differs from a collection of command-line tools.

设计与实现

Design & Implementation

1.4 强大的开发环境

1.4 Powerful development environments

复杂的开发环境可能是一把双刃剑。可以说,Common Lisp 环境的质量促成了它的广泛接受。另一方面,Smalltalk 图形环境的特殊性(坚持使用特定的字体、窗口样式等)使得很难将该语言移植到通过文本界面访问的系统或具有不同“外观和感觉”的图形系统。

Sophisticated development environments can be a two-edged sword. The quality of the Common Lisp environment has arguably contributed to its widespread acceptance. On the other hand, the particularity of the graphical environment for Smalltalk (with its insistence on specific fonts, window styles, etc.) made it difficult to port the language to systems accessed through a textual interface, or to graphical systems with a different “look and feel.”

1.6 编译概述

1.6 An Overview of Compilation

编译器是研究最深入的计算机程序之一。我们将在本书的其余部分,尤其是第 2 章第 4 章第 15 章和17章中反复讨论它们。本节的其余部分提供了介绍性概述。

Compilers are among the most well-studied computer programs. We will consider them repeatedly throughout the rest of the book, and in chapters 2, 4, 15, and 17 in particular. The remainder of this section provides an introductory overview.

例 1.19

Example 1.19

编译和解释阶段

Phases of compilation and interpretation

在典型的编译器中,编译过程会经历一系列明确定义的阶段,如图1.3所示。每个阶段都会发现对后续阶段有用的信息,或者将程序转换为对后续阶段更有用的形式。

In a typical compiler, compilation proceeds through a series of well-defined phases, shown in Figure 1.3. Each phase discovers information of use to later phases, or transforms the program into a form that is more useful to the subsequent phase.

传真:01-03-9780124104099
图 1.3 编译阶段。右侧列出了各个阶段,左侧列出了各个阶段之间传递信息的形式。符号表在整个编译过程中充当标识符信息的存储库。

前几个阶段(直到语义分析)用于弄清楚源程序的含义。它们有时被称为编译器的前端。最后几个阶段用于构建等效的目标程序。它们有时被称为编译器的后端。

The first few phases (up through semantic analysis) serve to figure out the meaning of the source program. They are sometimes called the front end of the compiler. The last few phases serve to construct an equivalent target program. They are sometimes called the back end of the compiler.

解释器(图 1.4)与编译器共享前端结构,但直接“执行”(解释)中间形式,而不是将其翻译成机器语言。执行通常采用一组相互递归的子例程的形式,这些子例程遍历(“遍历”)语法树,按程序顺序“执行”其节点。许多编译器和解释器阶段可以根据源语言和/或目标语言的正式描述自动创建。■

An interpreter (Figure 1.4) shares the compiler's front-end structure, but “executes” (interprets) the intermediate form directly, rather than translating it into machine language. The execution typically takes the form of a set of mutually recursive subroutines that traverse (“walk”) the syntax tree, “executing” its nodes in program order. Many compiler and interpreter phases can be created automatically from a formal description of the source and/or target languages. ■

传真:01-04-9780124104099
图 1.4 解释阶段。前端与编译器的前端基本相同。最后阶段“执行”中间形式,通常使用一组相互递归的子程序遍历语法树。

有时我们会听到有人将编译描述为一系列过程。过程是一个阶段或一组阶段,它们相对于编译的其余部分是序列化的:它在前面的阶段完成后才开始,并在任何后续阶段开始之前完成。如果需要,可以将过程编写为单独的程序,从文件读取其输入并将其输出写入文件。编译器通常分为几个过程,以便前端可以由多种机器(目标语言)的编译器共享,并且后端可以由多种源语言的编译器共享。在某些实现中,前端和后端可以由负责独立于语言和机器的代码改进的“中间端”分隔开。先前由于 20 世纪 80 年代中后期内存容量的急剧增加,编译器有时也被分为几个过程来尽量减少内存使用:每完成一个过程,下一个过程就可以重用其代码空间。

One will sometimes hear compilation described as a series of passes. A pass is a phase or set of phases that is serialized with respect to the rest of compilation: it does not start until previous phases have completed, and it finishes before any subsequent phases start. If desired, a pass may be written as a separate program, reading its input from a file and writing its output to a file. Compilers are commonly divided into passes so that the front end may be shared by compilers for more than one machine (target language), and so that the back end may be shared by compilers for more than one source language. In some implementations the front end and the back end may be separated by a “middle end” that is responsible for language- and machine-independent code improvement. Prior to the dramatic increases in memory sizes of the mid to late 1980s, compilers were also sometimes divided into passes to minimize memory usage: as each pass completed, the next could reuse its code space.

1.6.1 词汇和语法分析

1.6.1 Lexical and Syntax Analysis

例 1.20

Example 1.20

C 语言中的 GCD 程序

GCD program in C

考虑本章开头介绍的最大公约数 (GCD) 问题,如图1.2所示。假设一些简单的 I/O 例程,并将该函数重铸为一个独立程序,我们的代码在 C 语言中可能如下所示:

Consider the greatest common divisor (GCD) problem introduced at the beginning of this chapter, and shown as a function in Figure 1.2. Hypothesizing trivial I/O routines and recasting the function as a stand-alone program, our code might look like this in C:

int 主要() {

int main() {

 int i = getint(), j = getint();

 int i = getint(), j = getint();

 当 (i != j) {

 while (i != j) {

  若 (i > j) i = i − j;

  if (i > j) i = i − j;

  否则 j = j − i;

  else j = j − i;

 }

 }

 放入(i);

 putint(i);

}

}

例 1.21

Example 1.21

GCD 程序代币

GCD program tokens

扫描和解析用于识别程序的结构,而不考虑其含义。扫描器读取字符(' i '、' n '、' t '、' '、' m '、' a '、' i '、' n '、'(' ')' 等)并将它们分组为标记,这是程序的最小有意义单元。在我们的示例中,标记是

Scanning and parsing serve to recognize the structure of the program, without regard to its meaning. The scanner reads characters ('i', 'n', 't', ' ', 'm', 'a', 'i', 'n', '(',')', etc.) and groups them into tokens, which are the smallest meaningful units of the program. In our example, the tokens are

整数主要的{整数=
获得=获得
尽管!=
{如果
=别的=
}普京
}

t0020

扫描又称为词法分析。扫描器的主要目的是通过减少输入的大小(字符比标记多得多)和删除空格等无关字符来简化解析器的任务。扫描器通常还会删除注释并用行号和列号标记标记,以便在后续阶段更容易生成良好的诊断。可以设计一个解析器以字符而不是标记作为输入 - 省去扫描器 - 但结果会很笨拙且缓慢。

Scanning is also known as lexical analysis. The principal purpose of the scanner is to simplify the task of the parser, by reducing the size of the input (there are many more characters than tokens) and by removing extraneous characters like white space. The scanner also typically removes comments and tags tokens with line and column numbers, to make it easier to generate good diagnostics in later phases. One could design a parser to take characters instead of tokens as input— dispensing with the scanner—but the result would be awkward and slow.

例 1.22

Example 1.22

上下文无关语法和解析

Context-free grammar and parsing

解析将标记组织成解析树,该解析树根据其组成部分来表示高级构造(语句、表达式、子例程等)。每个构造都是树中的一个节点;其组成部分是其子节点。树的根就是“程序”;从左到右的叶子是从扫描器接收到的标记。从整体上看,树显示了标记的排列方式组合起来形成一个有效的程序。该结构依赖于一组称为上下文无关文法的潜在递归规则。每个规则都有一个箭头符号 (→),左侧是构造名称,右侧是可能的扩展。9例如,在 C 语言中,while循环由关键字while后跟带括号的布尔表达式和语句组成:

Parsing organizes tokens into a parse tree that represents higher-level constructs (statements, expressions, subroutines, and so on) in terms of their constituents. Each construct is a node in the tree; its constituents are its children. The root of the tree is simply “program”; the leaves, from left to right, are the tokens received from the scanner. Taken as a whole, the tree shows how the tokens fit together to make a valid program. The structure relies on a set of potentially recursive rules known as a context-free grammar. Each rule has an arrow sign (→) with the construct name on the left and a possible expansion on the right.9 In C, for example, a while loop consists of the keyword while followed by a parenthesized Boolean expression and a statement:

迭代语句→ while (表达式)语句

iteration-statement → while ( expression ) statement

反过来,该语句通常是用括号括起来的列表:

The statement, in turn, is often a list enclosed in braces:

语句复合语句

statementcompound-statement

复合语句→ { block-item-list_opt }

compound-statement → { block-item-list_opt }

在哪里

where

区块项目列表_opt区块项目列表

block-item-list_optblock-item-list

或者

or

块项目列表_optε

block-item-list_optε

and

区块项目列表区块项目

block-item-listblock-item

区块项列表区块项列表 区块项

block-item-listblock-item-list block-item

块项声明

block-itemdeclaration

块项语句

block-itemstatement

这里ε代表空字符串;它表示block-item-list_opt可以简单地删除。当然,还需要更多的语法规则来解释程序的完整结构。

Here ε represents the empty string; it indicates that block-item-list_opt can simply be deleted. Many more grammar rules are needed, of course, to explain the full structure of a program.

例 1.23

Example 1.23

GCD 程序解析树

GCD program parse tree

上下文无关文法定义了语言的语法;因此,解析又称为语法分析。C 语言有许多可能的语法(事实上是无限的);上面显示的片段取自官方语言定义 [ Int99 ] 中包含的示例语法。图 1.5显示了我们的 GCD 程序的完整解析树(基于此处未显示的完整语法)。虽然树的大小似乎令人望而生畏,但它的细节在本文的这一点上并不特别重要。重要的(1) 每个单独的分支点代表单个语法规则的应用,以及 (2) 由此产生的复杂性更多地反映了语法而不是输入程序。大部分来自 (a) 使用诸如block_item-listblock_item-list_opt之类的人工“构造”来生成任意长度,以及 (b) 使用同样人为的赋值表达式加法表达式乘法表达式等来捕获算术表达式中的优先级和结合性。我们将在下一小节中看到,一旦解析完成,就可以丢弃大部分这种复杂性。■

A context-free grammar is said to define the syntax of the language; parsing is therefore known as syntax analysis. There are many possible grammars for C (an infinite number, in fact); the fragment shown above is taken from the sample grammar contained in the official language definition [Int99]. A full parse tree for our GCD program (based on a full grammar not shown here) appears in Figure 1.5. While the size of the tree may seem daunting, its details aren't particularly important at this point in the text. What is important is that (1) each individual branching point represents the application of a single grammar rule, and (2) the resulting complexity is more a reflection of the grammar than it is of the input program. Much of the bulk stems from (a) the use of such artificial “constructs” as block_item-list and block_item-list_opt to generate lists of arbitrary length, and (b) the use of the equally artificial assignment-expression, additive-expression, multiplicative-expression, and so on, to capture precedence and associativity in arithmetic expressions. We shall see in the following subsection that much of this complexity can be discarded once parsing is complete. ■

传真:01-05-9780124104099传真:01-08-9780124104099
图 1.5 GCD 程序的解析树。符号ε表示空字符串。虚线表示一对一替换链,省略以节省空间;相邻的数字表示省略的节点数。虽然树的细节对本章来说并不重要,但大量的细节却很重要:它来自于必须将(简单得多的)源代码放入上下文无关语法的层次结构中。

在扫描和解析过程中,编译器或解释器会检查程序的所有标记是否格式正确,以及标记序列是否符合上下文无关语法定义的语法。任何格式错误的标记(例如,C 语言中的123abc$@foo )都会导致扫描器生成错误消息。任何语法无效的标记序列(例如,C 语言中的A = XYZ )都会导致解析器生成错误消息。

In the process of scanning and parsing, the compiler or interpreter checks to see that all of the program's tokens are well formed, and that the sequence of tokens conforms to the syntax defined by the context-free grammar. Any malformed tokens (e.g., 123abc or $@foo in C) should cause the scanner to produce an error message. Any syntactically invalid token sequence (e.g., A = X Y Z in C) should lead to an error message from the parser.

1.6.2 语义分析和中间代码生成

1.6.2 Semantic Analysis and Intermediate Code Generation

语义分析是发现程序中的含义。除其他功能外,语义分析器还可以识别同一标识符的多次出现是否意味着指向同一程序实体,并确保使用一致。在大多数语言中,它还会跟踪标识符和表达式的类型,以验证使用是否一致,并指导编译器后端的代码生成。

Semantic analysis is the discovery of meaning in a program. Among other things, the semantic analyzer recognizes when multiple occurrences of the same identifier are meant to refer to the same program entity, and ensures that the uses are consistent. In most languages it also tracks the types of both identifiers and expressions, both to verify consistent usage and to guide the generation of code in the back end of a compiler.

为了协助其工作,语义分析器通常会构建和维护一个符号表数据结构,该结构将每个标识符映射到已知的有关该标识符的信息。除其他事项外,此信息包括标识符的类型、内部结构(如果有)和范围(该标识符有效的程序部分)。

To assist in its work, the semantic analyzer typically builds and maintains a symbol table data structure that maps each identifier to the information known about it. Among other things, this information includes the identifier's type, internal structure (if any), and scope (the portion of the program in which it is valid).

使用符号表,语义分析器可以执行上下文无关语法和解析树的层次结构无法捕获的大量规则。例如,在 C 语言中,它会检查以确保

Using the symbol table, the semantic analyzer enforces a large variety of rules that are not captured by the hierarchical structure of the context-free grammar and the parse tree. In C, for example, it checks to make sure that

 每个标识符在使用前都经过声明。

 Every identifier is declared before it is used.

 未在不适当的上下文中使用任何标识符(将整数作为子例程调用、将字符串添加到整数、引用错误结构类型的字段等)。

 No identifier is used in an inappropriate context (calling an integer as a subroutine, adding a string to an integer, referencing a field of the wrong type of struct, etc.).

 子例程调用提供了正确数量和类型的参数。

 Subroutine calls provide the correct number and types of arguments.

 Switch语句臂上的标签是不同的常量。

 Labels on the arms of a switch statement are distinct constants.

■任何具有 非void返回类型的函数都会明确返回一个值。

 Any function with a non-void return type returns a value explicitly.

在许多前端中,语义分析器的工作采用语义动作例程的形式,当解析器意识到已经到达语法规则中的特定点时,它会调用这些例程。

In many front ends, the work of the semantic analyzer takes the form of semantic action routines, invoked by the parser when it realizes that it has reached a particular point within a grammar rule.

当然,并非所有语义规则都可以在编译时(或在解释器的前端)进行检查。那些可以检查的规则称为语言的静态语义。那些必须在运行时(或在解释器的后期阶段)检查的规则称为语言的动态语义。C 语言几乎没有动态检查(其设计者选择了性能而不是安全性)。其他语言在运行时强制执行的规则示例包括:

Of course, not all semantic rules can be checked at compile time (or in the front end of an interpreter). Those that can are referred to as the static semantics of the language. Those that must be checked at run time (or in the later phases of an interpreter) are referred to as the dynamic semantics of the language. C has very little in the way of dynamic checks (its designers opted for performance over safety). Examples of rules that other languages enforce at run time include:

 除非变量已被赋值,否则它们永远不会在表达式中使用。10

 Variables are never used in an expression unless they have been given a value.10

 除非指针指向一个有效对象,否则指针永远不会被取消引用。

 Pointers are never dereferenced unless they refer to a valid object.

 数组下标表达式位于数组的边界内。

 Array subscript expressions lie within the bounds of the array.

 算术运算不会溢出。

 Arithmetic operations do not overflow.

当无法静态地执行规则时,编译器通常会生成代码以在运行时执行适当的检查,如果其中一项检查失败,则中止程序或生成异常。(异常将在9.4 节中讨论。)不幸的是,某些规则可能过于昂贵或无法执行,并且语言实现可能根本无法检查它们。在 Ada 中,违反此类规则的程序被称为错误的;在 C 中,其行为被称为未定义的

When it cannot enforce rules statically, a compiler will often produce code to perform appropriate checks at run time, aborting the program or generating an exception if one of the checks then fails. (Exceptions will be discussed in Section 9.4.) Some rules, unfortunately, may be unacceptably expensive or impossible to enforce, and the language implementation may simply fail to check them. In Ada, a program that breaks such a rule is said to be erroneous; in C its behavior is said to be undefined.

例 1.24

Example 1.24

GCD 程序抽象语法树

GCD program abstract syntax tree

解析树有时也称为具体语法树,因为它完整而具体地演示了如何根据上下文无关文法规则导出特定的标记序列。但是,一旦我们知道标记序列有效,解析树中的大部分信息就与编译的后续阶段无关。在检查静态语义规则的过程中,语义分析器通常通过删除树内部的大部分“人工”节点,将解析树转换为抽象语法树(也称为AST或简称为语法树)。语义分析器还会用有用的信息注释剩余节点,例如从标识符指向其符号表条目的指针。附加到特定节点的注释称为其属性。图 1.6显示了我们的 GCD 程序的语法树。■

A parse tree is sometimes known as a concrete syntax tree, because it demonstrates, completely and concretely, how a particular sequence of tokens can be derived under the rules of the context-free grammar. Once we know that a token sequence is valid, however, much of the information in the parse tree is irrelevant to further phases of compilation. In the process of checking static semantic rules, the semantic analyzer typically transforms the parse tree into an abstract syntax tree (otherwise known as an AST, or simply a syntax tree) by removing most of the “artificial” nodes in the tree's interior. The semantic analyzer also annotates the remaining nodes with useful information, such as pointers from identifiers to their symbol table entries. The annotations attached to a particular node are known as its attributes. A syntax tree for our GCD program is shown in Figure 1.6. ■

传真:01-06-9780124104099
图 1.6 GCD 程序的语法树和符号表。请注意与图 1.5的对比:语法树仅保留了程序的基本结构,省略了仅驱动解析算法所需的细节。

例 1.25

Example 1.25

解释语法树

Interpreting the syntax tree

许多解释器使用带注释的语法树来表示正在运行的程序:“执行”相当于树的遍历。在我们的 GCD 程序中,解释器将从图 1.6的根节点开始,按顺序访问树主干上的语句。在第一个“ := ”节点,解释器会注意到右子节点是一个call:因此它会调用getint例程(位于符号表的第 3 个槽位)并将结果赋值给i(位于符号表的第 5 个槽位)。在第二个“ := ”节点,解释器会类似地将getint的结果赋值给j。在while节点,它会反复评估左(“≠”)子节点,如果结果为真,则递归地遍历右( if )子节点下的树。最后,一旦while节点的左子节点评估为假,解释器就会转到最后的call节点,并输出其结果。■

Many interpreters use an annotated syntax tree to represent the running program: “execution” then amounts to tree traversal. In our GCD program, an interpreter would start at the root of Figure 1.6 and visit, in order, the statements on the main spine of the tree. At the first “:=” node, the interpreter would notice that the right child is a call: it would therefore call the getint routine (found in slot 3 of the symbol table) and assign the result into i (found in slot 5 of the symbol table). At the second “:=” node the interpreter would similarly assign the result of getint into j. At the while node it would repeatedly evaluate the left (“≠”) child and, if the result was true, recursively walk the tree under the right (if) child. Finally, once the while node's left child evaluated to false, the interpreter would move on to the final call node, and output its result. ■

在许多编译器中,带注释的语法树构成了从前端传递到后端的中间形式。在其他编译器中,语义分析以遍历树(通常是单次遍历)结束,从而生成其他中间形式。一种常见的中间形式由一个控制流图组成,其节点类似于简单理想化机器的汇编语言片段。我们将在第 15 章进一步考虑这个选项,其中我们的 GCD 程序的控制流图如图15.3所示。在一组相关的编译器中,几种语言的前端和几台机器的后端将共享一个通用的中间形式。

In many compilers, the annotated syntax tree constitutes the intermediate form that is passed from the front end to the back end. In other compilers, semantic analysis ends with a traversal of the tree (typically single pass) that generates some other intermediate form. One common such form consists of a control flow graph whose nodes resemble fragments of assembly language for a simple idealized machine. We will consider this option further in Chapter 15, where a control flow graph for our GCD program appears in Figure 15.3. In a suite of related compilers, the front ends for several languages and the back ends for several machines would share a common intermediate form.

1.6.3 目标代码生成

1.6.3 Target Code Generation

例 1.26

Example 1.26

GCD程序汇编代码

GCD program assembly code

编译器的代码生成阶段将中间形式翻译成目标语言。根据语法树中包含的信息,生成正确的代码通常不是一项艰巨的任务(生成好的代码更难,我们将在1.6.4 节中看到)。为了生成汇编语言或机器语言,代码生成器遍历符号表以将位置分配给变量,然后遍历程序的中间表示,为变量引用生成加载和存储,中间穿插适当的算术运算、测试和分支。图 1.7显示了我们的 GCD 示例的简单代码,以 x86 汇编语言编写。它是由一个简单的教学编译器自动生成的。

The code generation phase of a compiler translates the intermediate form into the target language. Given the information contained in the syntax tree, generating correct code is usually not a difficult task (generating good code is harder, as we shall see in Section 1.6.4). To generate assembly or machine language, the code generator traverses the symbol table to assign locations to variables, and then traverses the intermediate representation of the program, generating loads and stores for variable references, interspersed with appropriate arithmetic operations, tests, and branches. Naive code for our GCD example appears in Figure 1.7, in x86 assembly language. It was generated automatically by a simple pedagogical compiler.

编号01-07-9780124104099
图 1.7 用于 GCD 程序的简单 x86 汇编语言。

汇编语言助记符可能看起来有点神秘,但每行上的注释(不是由编译器生成的!)应该可以使图 1.61.7之间的对应关系大致明显。一些提示:espebpeaxebxedi是寄存器(特殊存储位置,数量有限,可以非常快速地访问)。−8(%ebp)指的是地址在寄存器ebp中的位置之前 8 个字节的内存位置;在这个程序中,ebp用作我们可以从中找到变量ij的基数。子程序调用指令的参数通过将它推送到堆栈上来传递,其中esp是堆栈顶部指针。返回值返回到寄存器eax中。算术运算用运算结果覆盖其第二个参数。11

The assembly language mnemonics may appear a bit cryptic, but the comments on each line (not generated by the compiler!) should make the correspondence between Figures 1.6 and 1.7 generally apparent. A few hints: esp, ebp, eax, ebx, and edi are registers (special storage locations, limited in number, that can be accessed very quickly). −8(%ebp) refers to the memory location 8 bytes before the location whose address is in register ebp; in this program, ebp serves as a base from which we can find variables i and j. The argument to a subroutine call instruction is passed by pushing it onto a stack, for which esp is the top-of-stack pointer. The return value comes back in register eax. Arithmetic operations overwrite their second argument with the result of the operation.11

代码生成器通常会将符号表包含在目标代码的不可执行部分中,以供符号调试器稍后使用。

Often a code generator will save the symbol table for later use by a symbolic debugger, by including it in a nonexecutable part of the target code.

1.6.4 代码改进

1.6.4 Code Improvement

代码改进通常被称为优化,尽管它很少使任何事情在绝对意义上达到最优。它是编译的一个可选阶段,其目标是将程序转换为一个新版本,以更高效地计算相同的结果——更快或使用更少的内存,或两者兼而有之。

Code improvement is often referred to as optimization, though it seldom makes anything optimal in any absolute sense. It is an optional phase of compilation whose goal is to transform a program into a new version that computes the same result more efficiently—more quickly or using less memory, or both.

一些改进与机器无关。这些改进可以作为中间形式的转换来执行。其他改进需要了解目标机器(或任何将以目标语言执行程序的东西)。这些必须作为目标程序的转换来执行。因此,代码改进通常会在编译器阶段列表中出现两次:一次是在语义分析和中间代码生成之后,另一次是在目标代码生成之后。

Some improvements are machine independent. These can be performed as transformations on the intermediate form. Other improvements require an understanding of the target machine (or of whatever will execute the program in the target language). These must be performed as transformations on the target program. Thus code improvement often appears twice in the list of compiler phases: once immediately after semantic analysis and intermediate code generation, and again immediately after target code generation.

例 1.27

Example 1.27

GCD程序优化

GCD program optimization

对图 1.7中的代码应用好的代码改进器将生成示例 1.2中所示的代码。比较这两个程序,我们可以看到改进后的版本短得多。明显缺少了大多数加载和存储操作。独立于机器的代码改进器能够验证ij是否可以在主循环执行期间保存在寄存器中。(例如,如果循环包含对可能重用这些寄存器或可能尝试修改ij 的子例程的调用,则情况就不会如此。)然后,特定于机器的代码改进器能够将ij分配给目标机器的实际寄存器。对于具有复杂内部行为的现代微处理器,编译器通常可以生成比人类汇编语言程序员更好的代码。

Applying a good code improver to the code in Figure 1.7 produces the code shown in Example 1.2. Comparing the two programs, we can see that the improved version is quite a lot shorter. Conspicuously absent are most of the loads and stores. The machine-independent code improver is able to verify that i and j can be kept in registers throughout the execution of the main loop. (This would not have been the case if, for example, the loop contained a call to a subroutine that might reuse those registers, or that might try to modify i or j.) The machine-specific code improver is then able to assign i and j to actual registers of the target machine. For modern microprocessors, with complex internal behavior, compilers can usually generate better code than can human assembly language programmers.

01-01-9780124104099检查你的理解

Check Your Understanding

22. 列出编译的主要阶段,并描述每个阶段所执行的工作。

22. List the principal phases of compilation, and describe the work performed by each.

23. 列出作为解释的一部分执行的阶段。

23. List the phases that are also executed as part of interpretation.

24. 描述程序从扫描器传递到解析器、从解析器传递到语义分析器、从语义分析器传递到中间代码生成器的形式。

24. Describe the form in which a program is passed from the scanner to the parser; from the parser to the semantic analyzer; from the semantic analyzer to the intermediate code generator.

25. 编译器的前端与后端有何区别?

25. What distinguishes the front end of a compiler from the back end?

26. 编译阶段和编译过程有什么区别?在什么情况下编译器进行多次编译是有意义的?

26. What is the difference between a phase and a pass of compilation? Under what circumstances does it make sense for a compiler to have multiple passes?

27. 编译器的符号表有什么用途?

27. What is the purpose of the compiler's symbol table?

28. 静态语义和动态语义有什么区别?

28. What is the difference between static and dynamic semantics?

29. 在现代机器上,汇编语言程序员是否仍然倾向于编写比好的编译器更好的代码?为什么或为什么不?

29. On modern machines, do assembly language programmers still tend to write better code than a good compiler can? Why or why not?

1.7 总结和结束语

1.7 Summary and Concluding Remarks

在本章中,我们介绍了编程语言设计和实现的研究。我们考虑了为什么有这么多语言,是什么让它们成功或失败,如何对它们进行分类以供研究,以及读者可能从这项研究中获得什么好处。我们注意到语言设计和语言实现是紧密相连的。显然,实现必须符合语言的规则。同时,语言设计者必须考虑实现各种功能的难易程度,以及可能产生什么样的性能。

In this chapter we introduced the study of programming language design and implementation. We considered why there are so many languages, what makes them successful or unsuccessful, how they may be categorized for study, and what benefits the reader is likely to gain from that study. We noted that language design and language implementation are intimately tied to one another. Obviously an implementation must conform to the rules of the language. At the same time, a language designer must consider how easy or difficult it will be to implement various features, and what sort of performance is likely to result.

语言实现通常分为基于解释的实现和基于编译的实现。然而,我们注意到,这两种方法之间的区别并不明显,大多数实现都包含这两种方法的一点。一般来说,如果执行之前有一个翻译步骤,该步骤 (1) 全面分析程序的结构(语法)和含义(语义),并且 (2) 生成一个形式截然不同的等效程序,那么我们就说该语言是编译的。本书中的大部分实现材料都与编译有关。

Language implementations are commonly differentiated into those based on interpretation and those based on compilation. We noted, however, that the difference between these approaches is fuzzy, and that most implementations include a bit of each. As a general rule, we say that a language is compiled if execution is preceded by a translation step that (1) fully analyzes both the structure (syntax) and meaning (semantics) of the program, and (2) produces an equivalent program in a significantly different form. The bulk of the implementation material in this book pertains to compilation.

编译器通常由一系列阶段组成。前几个阶段(扫描、解析和语义分析)用于分析源程序。这些阶段统称为编译器的前端。最后几个阶段(目标代码生成和针对机器的代码改进)称为后端。它们用于构建目标程序(最好是快速程序),其语义与源代码相匹配。在前端和后端之间,优秀的编译器会执行大量与机器无关的代码改进;这个“中端”的阶段通常构成编译器的大部分代码,并占其执行时间的大部分。

Compilers are generally structured as a series of phases. The first few phases— scanning, parsing, and semantic analysis—serve to analyze the source program. Collectively these phases are known as the compiler's front end. The final few phases—target code generation and machine-specific code improvement—are known as the back end. They serve to build a target program—preferably a fast one—whose semantics match those of the source. Between the front end and the back end, a good compiler performs extensive machine-independent code improvement; the phases of this “middle end” typically comprise the bulk of the code of the compiler, and account for most of its execution time.

第 3、6、7、8、9 和 10 章是本书其余部分的核心。这些章从程序员和语言实现者的角度介绍了语言设计的基本问题。为了支持实现的讨论,第2章和4介绍中更详细地描述了编译器前端。第 5 章概述了汇编级架构。第 1517章讨论了编译器后端,包括汇编器和链接器、运行时系统和代码改进技术。第1114章介绍了其他语言范式。附录 A列出了文中提到的主要编程语言,以及家谱和参考书目。附录 B包含“设计和实现”边栏列表;附录 C包含编号示例列表。

Chapters 3, 6, 7, 8, 9, and 10 form the core of the rest of this book. They cover fundamental issues of language design, both from the point of view of the programmer and from the point of view of the language implementor. To support the discussion of implementations, Chapters 2 and 4 describe compiler front ends in more detail than has been possible in this introduction. Chapter 5 provides an overview of assembly-level architecture. Chapters 15 through 17 discuss compiler back ends, including assemblers and linkers, run-time systems, and code improvement techniques. Additional language paradigms are covered in Chapters 11 through 14. Appendix A lists the principal programming languages mentioned in the text, together with a genealogical chart and bibliographic references. Appendix B contains a list of “Design & Implementation” sidebars; Appendix C contains a list of numbered examples.

1.8 练习

1.8 Exercises

1.1 计算机程序中的错误可以根据检测到的时间进行分类,如果在编译时检测到错误,则根据编译器的哪个部分检测到错误进行分类。使用您最喜欢的命令式语言,给出以下每个示例。

1.1 Errors in a computer program can be classified according to when they are detected and, if they are detected at compile time, what part of the compiler detects them. Using your favorite imperative language, give an example of each of the following.

(a) 扫描仪检测到词汇错误

(a) A lexical error, detected by the scanner

(b) 解析器检测到语法错误

(b) A syntax error, detected by the parser

(c) 通过语义分析检测到的静态语义错误

(c) A static semantic error, detected by semantic analysis

(d) 编译器生成的代码检测到的动态语义错误

(d) A dynamic semantic error, detected by code generated by the compiler

(e) 编译器无法捕获的错误,也无法轻易生成代码来捕获的错误(这应该是违反语言定义的,而不仅仅是程序错误)

(e) An error that the compiler can neither catch nor easily generate code to catch (this should be a violation of the language definition, not just a program bug)

1.2 再次考虑 Niklaus Wirth 分发的 Pascal 工具集(示例 1.15)。成功构建机器语言版本的 Pascal 编译器后,原则上可以丢弃 P 代码解释器和 P 代码版本的编译器。为什么人们会选择这样做呢?

1.2 Consider again the Pascal tool set distributed by Niklaus Wirth (Example 1.15). After successfully building a machine language version of the Pascal compiler, one could in principle discard the P-code interpreter and the P-code version of the compiler. Why might one choose not to do so?

1.3  Fortran 和 C 等命令式语言通常是编译型的,而脚本语言通常是解释型的,因为脚本语言中的许多问题直到运行时才能解决。解释仅仅是编译不可行时“不得不做的事情”吗?还是即使有编译器可用,解释语言实际上也有一些优势?

1.3 Imperative languages like Fortran and C are typically compiled, while scripting languages, in which many issues cannot be settled until run time, are typically interpreted. Is interpretation simply what one “has to do” when compilation is infeasible, or are there actually some advantages to interpreting a language, even when a compiler is available?

1.4 示例 1.20中的gcd程序也可以这样写:int main() { int i = getint(), j = getint(); while (i != j) { if (i > j) i = i % j; else j = j % i; } putint(i); }这个程序计算的结果是否相同?如果不是,你能修复它吗?在什么情况下你认为其中一个会更快?

 

  

  

   

   

  

  

 

1.4 The gcd program of Example 1.20 might also be written

 int main() {

  int i = getint(), j = getint();

  while (i != j) {

   if (i > j) i = i % j;

   else j = j % i;

  }

  putint(i);

 }

Does this program compute the same result? If not, can you fix it? Under what circumstances would you expect one or the other to be faster?

1.5 扩展示例 1.25 ,跟踪对输入 12 和 8 的gcd程序的解释。哪些语法树节点被访问,按什么顺序访问?

1.5 Expanding on Example 1.25, trace an interpretation of the gcd program on the inputs 12 and 8. Which syntax tree nodes are visited, in which order?

1.6 解释和代码生成都可以通过遍历语法树来完成。比较这两种遍历。它们在哪些方面相似/不同?

1.6 Both interpretation and code generation can be performed by traversal of a syntax tree. Compare these two kinds of traversals. In what ways are they similar/different?

1.7 在你的 C 语言本地实现中,整数的大小限制是多少?如果发生算术溢出,会发生什么?大小限制对程序从一台机器/编译器到另一台机器/编译器的可移植性有何影响?对于 Java、Ada、Pascal 和 Scheme,这些问题的答案有何不同?(您可能需要查找手册。)

1.7 In your local implementation of C, what is the limit on the size of integers? What happens in the event of arithmetic overflow? What are the implications of size limits on the portability of programs from one machine/compiler to another? How do the answers to these questions differ for Java? For Ada? For Pascal? For Scheme? (You may need to find a manual.)

1.8  Unix make实用程序允许程序员指定程序中单独编译的部分之间的依赖关系。如果文件A依赖于文件B,并且文件B被修改,则make推断必须重新编译A ,以防对B的任何更改会影响为A生成的代码。这种依赖管理有多准确?在什么情况下会导致不必要的工作?在什么情况下,它会无法重新编译需要重新编译的内容?

1.8 The Unix make utility allows the programmer to specify dependences among the separately compiled pieces of a program. If file A depends on file B and file B is modified, make deduces that A must be recompiled, in case any of the changes to B would affect the code produced for A. How accurate is this sort of dependence management? Under what circumstances will it lead to unnecessary work? Under what circumstances will it fail to recompile something that needs to be recompiled?

1.9 为什么很难判断程序是否正确?如何查找代码中的错误?测试可以发现哪些错误?哪些错误不能?(有关程序正确性的更正式概念,请参阅第 4 章末尾的参考书目注释。)

1.9 Why is it difficult to tell whether a program is correct? How do you go about finding bugs in your code? What kinds of bugs are revealed by testing? What kinds of bugs are not? (For more formal notions of program correctness, see the bibliographic notes at the end of Chapter 4.)

1.9 探索

1.9 Explorations

1.10

1.10

(a) 您学习的第一门编程语言是什么?如果您选择了它,为什么会选择它?如果是别人为您选择的,您认为他们为什么会选择它?您觉得该语言的哪些部分最难学?

(a) What was the first programming language you learned? If you chose it, why did you do so? If it was chosen for you by others, why do you think they chose it? What parts of the language did you find the most difficult to learn?

(b) 对于你最熟悉的语言(这可能是也可能不是你学到的第一门语言),列出三件你希望设计不同的事物。你认为它们为什么会这样设计?如果有机会重来,你会如何修复它们?会有什么负面影响吗,例如在编译器复杂性或程序执行速度方面?

(b) For the language with which you are most familiar (this may or may not be the first one you learned), list three things you wish had been differently designed. Why do you think they were designed the way they were? How would you fix them if you had the chance to do it over? Would there be any negative consequences, for example in terms of compiler complexity or program execution speed?

1.11 找一个主要使用图 1.1中不同类别语言的同学。(例如,如果你主要使用 C 语言,你可以找一个有 Lisp 经验的人。)比较一下笔记。在你们各自的经历中,编程最简单和最困难的方面是什么?选择一个简单的问题(例如,排序或识别图中的连通分量),并使用你最喜欢的每种语言来解决它。哪种解决方案更优雅(你们两个同意吗)?哪个更快?为什么?

1.11 Get together with a classmate whose principal programming experience is with a language in a different category of Figure 1.1. (If your experience is mostly in C, for example, you might search out someone with experience in Lisp.) Compare notes. What are the easiest and most difficult aspects of programming, in each of your experiences? Pick a simple problem (e.g., sorting, or identification of connected components in a graph) and solve it using each of your favorite languages. Which solution is more elegant (do the two of you agree)? Which is faster? Why?

1.12 

1.12 

(a) 如果您可以访问 Unix 系统,请使用−S命令行标志编译一个简单的程序。向生成的汇编语言文件添加注释以解释每条指令的用途。

(a) If you have access to a Unix system, compile a simple program with the −S command-line flag. Add comments to the resulting assembly language file to explain the purpose of each instruction.

(b) 现在使用-o命令行标志生成可重定位目标文件。使用适当的本地工具(特别是查找nmobjdump使用符号调试器(例如gdbdbx)来识别每行汇编程序对应的机器语言。

(b) Now use the −o command-line flag to generate a relocatable object file. Using appropriate local tools (look in particular for nm, objdump, or a symbolic debugger like gdb or dbx), identify the machine language corresponding to each line of assembler.

(c) 使用nmobjdump或类似工具,识别目标文件中未定义的外部符号。现在运行编译器直至完成,以生成可执行文件。最后,再次运行nmobjdump以查看部分 (b) 中的符号发生了什么。它们来自哪里 — 链接器如何解析它们?

(c) Using nm, objdump, or a similar tool, identify the undefined external symbols in your object file. Now run the compiler to completion, to produce an executable file. Finally, run nm or objdump again to see what has happened to the symbols in part (b). Where did they come from—how did the linker resolve them?

(d)使用 -v命令行标志再次运行编译器直至完成。您应该看到描述编译过程中调用的各种子程序的消息(某些编译器对此选项使用不同的字母;请查看手册页)。子程序可能包括预处理器、编译器本身的单独传递(通常为两次)、可能还有汇编器和链接器。如果可能,请自己单独运行这些子程序。它们中的哪一个生成了前面子问题中描述的文件?解释调用子程序的各种命令行标志的用途。

(d) Run the compiler to completion one more time, using the −v command-line flag. You should see messages describing the various subprograms invoked during the compilation process (some compilers use a different letter for this option; check the man page). The subprograms may include a preprocessor, separate passes of the compiler itself (often two), probably an assembler, and the linker. If possible, run these subprograms yourself, individually. Which of them produce the files described in the previous subquestions? Explain the purpose of the various command-line flags with which the subprograms were invoked.

1.13 编写一个程序,该程序会犯动态语义错误(例如,除以零、访问数组末尾以外的内容、取消引用空指针)。运行此程序时会发生什么?编译器是否为您提供了控制发生情况的选项?设计一个实验来评估运行时语义检查的成本。如果可能,请使用多种语言或编译器尝试此练习。

1.13 Write a program that commits a dynamic semantic error (e.g., division by zero, access off the end of an array, dereference of a null pointer). What happens when you run this program? Does the compiler give you options to control what happens? Devise an experiment to evaluate the cost of runtime semantic checks. If possible, try this exercise with more than one language or compiler.

1.14  C 语言被认为是一种相对“不安全”的高级语言。例如:它允许程序员以比其“更安全”的同类语言更多的方式混合不同大小和类型的操作数。Unix lint实用程序可用于搜索 C 程序中潜在的不安全构造。实际上,其他语言中编译器强制执行的许多规则在 C 语言中是可选的,并且由单独的程序强制执行(如果需要)。您如何看待这种方法?这是一个好主意吗?为什么或为什么不?

1.14 C has a reputation for being a relatively “unsafe” high-level language. For example: it allows the programmer to mix operands of different sizes and types in many more ways than its “safer” cousins. The Unix lint utility can be used to search for potentially unsafe constructs in C programs. In effect, many of the rules that are enforced by the compiler in other languages are optional in C, and are enforced (if desired) by a separate program. What do you think of this approach? Is it a good idea? Why or why not?

1.15 使用互联网搜索引擎或杂志索引服务,了解 Java 和 C# 的历史,包括 Sun 和 Microsoft 在 Java 标准化方面的冲突。有人声称 C# 至少在一定程度上是 Microsoft 试图破坏 Java 的传播。其他人则指出这两种语言在哲学和实践上的差异,并认为 C# 的优势不止于此。事后看来,您如何看待 Microsoft 寻求替代 Java 的决定?

1.15 Using an Internet search engine or magazine indexing service, read up on the history of Java and C#, including the conflict between Sun and Microsoft over Java standardization. Some have claimed that C# was, at least in part, an attempt by Microsoft to undermine the spread of Java. Others point to philosophical and practical differences between the languages, and argue that C# more than stands on its merits. In hindsight, how would you characterize Microsoft's decision to pursue an alternative to Java?

1.10 书目注释

1.10 Bibliographic Notes

本书中面向编译器的章节试图传达编译器的功能,而不是解释如何构建编译器。其他文本中可以找到更详细的信息。主要选项包括 Aho 的作品等人 [ ALSU07 ]、Cooper 和 Torczon [ CT04 ] 以及 Fischer 等人 [ FCL10 ] 的著作。其他优秀但不太流行的著作包括 Appel [ App97 ] 和 Grune 等人 [ GBJ + 12 ] 的著作。关于编程语言设计的热门著作包括 Louden [ LL12 ]、Sebesta [ Seb15 ] 和 Sethi [ Set96 ]的著作。

The compiler-oriented chapters of this book attempt to convey a sense of what the compiler does, rather than explaining how to build one. A much greater level of detail can be found in other texts. Leading options include the work of Aho et al. [ALSU07], Cooper and Torczon [CT04], and Fischer et al. [FCL10]. Other excellent, though less current texts include those of Appel [App97] and Grune et al. [GBJ+12]. Popular texts on programming language design include those of Louden [LL12], Sebesta [Seb15], and Sethi [Set96].

关于编程语言历史的一些最好的信息可以在计算机协会在 1978、1993 和 2007 年主办的会议记录中找到 [ Wex78Ass93Ass07 ]。另一个很好的参考资料是 Horowitz 1987 年的文本 [ Hor87 ]。在季刊《 IEEE 计算史年鉴》中可以找到更广泛的历史资料。鉴于个人品味在编程语言设计中的重要性,一些语言比较不可避免地会带有措辞强烈的意见。早期的例子包括 Dijkstra [ Dij82 ]、Hoare [ Hoa81 ]、Kernighan [ Ker81 ] 和 Wirth [ Wir85a ]的著作。

Some of the best information on the history of programming languages can be found in the proceedings of conferences sponsored by the Association for Computing Machinery in 1978,1993, and 2007 [Wex78, Ass93, Ass07]. Another excellent reference is Horowitz's 1987 text [Hor87]. A broader range of historical material can be found in the quarterly IEEE Annals of the History of Computing. Given the importance of personal taste in programming language design, it is inevitable that some language comparisons should be marked by strongly worded opinions. Early examples include the writings of Dijkstra [Dij82], Hoare [Hoa81], Kernighan [Ker81], and Wirth [Wir85a].

许多现代软件开发都是在集成编程环境中进行的。这些环境的有影响力的前身包括 Symbolics Corp. 的 Genera Common Lisp 环境 [ WMWM87 ] 以及 Xerox Palo Alto 研究中心的 Smalltalk [ Gol84 ]、Interlisp [ TM81 ] 和 Cedar [ SZBH86 ] 环境。

Much modern software development takes place in integrated programming environments. Influential precursors to these environments include the Genera Common Lisp environment from Symbolics Corp. [WMWM87] and the Smalltalk [Gol84], Interlisp [TM81], and Cedar [SZBH86] environments at the Xerox Palo Alto Research Center.


1示例中的 22 行汇编代码在机器语言中以不同数量的字节进行编码。例如,三个cmp(比较)指令恰好具有相同的寄存器操作数,并以双字节序列 ( 39 c3 ) 进行编码。四个mov (移动)指令具有不同的操作数和长度,并以898b开头。所选语法是 GNU gcc编译器套件的语法,其中结果会覆盖最后一个操作数,而不是第一个操作数。

1 The 22 lines of assembly code in the example are encoded in varying numbers of bytes in machine language. The three cmp (compare) instructions, for example, all happen to have the same register operands, and are encoded in the two-byte sequence (39 c3). The four mov (move) instructions have different operands and lengths, and begin with 89 or 8b. The chosen syntax is that of the GNU gcc compiler suite, in which results overwrite the last operand, not the first.

2高级语言也可以直接解释执行,无需翻译步骤。我们将在1.4 节中讨论此选项。这是 Python 和 JavaScript 等脚本语言的主要实现方式。

2 High-level languages may also be interpreted directly, without the translation step. We will return to this option in Section 1.4. It is the principal way in which scripting languages like Python and JavaScript are implemented.

3这些语言的名称有时全部用大写字母书写,有时则混合使用大小写。为了保持一致性,我在本书中采用了以下惯例:对于名称以单词形式发音的语言(例如 Fortran、Cobol、Basic),使用混合大小写;对于名称以一系列字母形式发音的语言(例如 APL、PL/I、ML),使用大写字母。

3 The names of these languages are sometimes written entirely in uppercase letters and sometimes in mixed case. For consistency's sake, I adopt the convention in this book of using mixed case for languages whose names are pronounced as words (e.g., Fortran, Cobol, Basic), and uppercase for those pronounced as a series of letters (e.g., APL, PL/I, ML).

4 Niklaus Wirth (1934-),瑞士苏黎世联邦理工学院信息学名誉教授,他发明了一系列有影响力的语言,包括 Euler、Algol W、Pascal、Modula、Modula-2 和 Oberon。除其他外,他的语言引入了枚举、子范围和集合类型的概念,并统一了记录(结构)和变体(联合)的概念。1984 年,他获得了计算机领域最高荣誉——年度 ACM 图灵奖。

4 Niklaus Wirth (1934–), Professor Emeritus of Informatics at ETH in Zürich, Switzerland, is responsible for a long line of influential languages, including Euler, Algol W, Pascal, Modula, Modula-2, and Oberon. Among other things, his languages introduced the notions of enumeration, subrange, and set types, and unified the concepts of records (structs) and variants (unions). He received the annual ACM Turing Award, computing's highest honor, in 1984.

5肯·汤普森(1943-)领导开发 Unix 的团队。他还设计了 B 编程语言,它是 BCPL 的子语言,也是 C 语言的父语言。丹尼斯·里奇(1941-2011)是 C 语言本身开发的主要力量。汤普森和里奇共同组成了一个非常高效和有影响力的团队的核心。1983 年,他们共同获得了 ACM 图灵奖。

5 Ken Thompson (1943–) led the team that developed Unix. He also designed the B programming language, a child of BCPL and the parent of C. Dennis Ritchie (1941–2011) was the principal force behind the development of C itself. Thompson and Ritchie together formed the core of an incredibly productive and influential group. They shared the ACM Turing Award in 1983.

6唐纳德·E·克努斯(Donald E. Knuth,1938-),斯坦福大学名誉教授,算法设计和分析领域的领军人物之一,也是众所周知的 T E X 排版系统(本书即以此系统编写)和T E X 所采用的文学编程方法。他的多卷本著作《计算机编程艺术》在大多数专业计算机科学家的书架上占有一席之地。他于 1974 年获得 ACM 图灵奖。

6 Donald E. Knuth (1938–), Professor Emeritus at Stanford University and one of the foremost figures in the design and analysis of algorithms, is also widely known as the inventor of the TEX typesetting system (with which this book was produced) and of the literate programming methodology with which TEX was constructed. His multivolume The Art of Computer Programming has an honored place on the shelf of most professional computer scientists. He received the ACM Turing Award in 1974.

7约翰·冯·诺依曼(1903-1957)是一位数学家和计算机先驱,他帮助开发了存储程序计算的概念,该概念是大多数计算机硬件的基础。在存储程序计算机中,程序和数据都表示为内存中的位,处理器会反复获取、解释和更新这些位。

7 John von Neumann (1903–1957) was a mathematician and computer pioneer who helped to develop the concept of stored program computing, which underlies most computer hardware. In a stored program computer, both programs and data are represented as bits in memory, which the processor repeatedly fetches, interprets, and updates.

8本书中我们将使用术语“x86”来指代 Intel 8086 及其后代的指令集架构,包括各种 Pentium、“Core”和 Xeon 处理器。Intel 称此架构为 IA-32,但 x86 是一个更通用的术语,也涵盖了 AMD 等竞争对手的产品。

8 Throughout this book we will use the term “x86” to refer to the instruction set architecture of the Intel 8086 and its descendants, including the various Pentium, “Core,” andXeon processors. Intel calls this architecture the IA-32, but x86 is a more generic term that encompasses the offerings of competitors such as AMD as well.

9理论家们还研究上下文相关语法,其中构造的允许扩展(适用规则)取决于构造出现的上下文(即左侧和右侧的构造)。上下文敏感性对于英语等自然语言很重要,但它几乎从未在编程语言设计中使用过。

9 Theorists also study context-sensitive grammars, in which the allowable expansions of a construct (the applicable rules) depend on the context in which the construct appears (i.e., on constructs to the left and right). Context sensitivity is important for natural languages like English, but it is almost never used in programming language design.

10正如我们将在第 6.1.3 节中看到的,Java 和 C# 实际上确实在编译时强制初始化,但仅仅是通过采用一组保守的“明确赋值”规则,禁止在编译时难以或无法验证正确性的程序。

10 As we shall see in Section 6.1.3, Java and C# actually do enforce initialization at compile time, but only by adopting a conservative set of rules for “definite assignment,” outlawing programs for which correctness is difficult or impossible to verify at compile time.

11如脚注 1 所述,这些是 GNU 汇编器约定;Microsoft 和 Intel 汇编器以相反的顺序指定参数。

11 As noted in footnote 1, these are GNU assembler conventions; Microsoft and Intel assemblers specify arguments in the opposite order.

2

编程语言语法

Programming Language Syntax

与英语或中文等自然语言不同,计算机语言必须精确。其形式(语法)和含义(语义)都必须明确无误,这样程序员和计算机才能知道程序应该做什么。为了提供所需的精确度,语言设计者和实现者使用正式的语法和语义符号。为了便于在后面的章节中讨论语言特性,我们将首先介绍这种符号:本章介绍语法,第4 章介绍语义。

Unlike natural languages such as English or Chinese, computer languages must be precise. Both their form (syntax) and meaning (semantics) must be specified without ambiguity, so that both programmers and computers can tell what a program is supposed to do. To provide the needed degree of precision, language designers and implementors use formal syntactic and semantic notation. To facilitate the discussion of language features in later chapters, we will cover this notation first: syntax in the current chapter and semantics in Chapter 4.

例 2.1

Example 2.1

阿拉伯数字的语法

Syntax of Arabic numerals

举一个启发性的例子,考虑一下我们用来表示数字的阿拉伯数字。这些数字由数字组成,我们可以按如下方式枚举(“|”表示“或”):

As a motivating example, consider the Arabic numerals with which we represent numbers. These numerals are composed of digits, which we can enumerate as follows ('|' means “or”):

数字 → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

数字是数字的语法构成要素。在通常的表示法中,我们说自然数由任意长度(非空)的数字串表示,以非零数字开头:

Digits are the syntactic building blocks for numbers. In the usual notation, we say that a natural number is represented by an arbitrary-length (nonempty) string of digits, beginning with a nonzero digit:

非零数字 → 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

non_zero_digit → 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

自然数 → 非零数字 *

natural_number → non_zero_digit digit *

此处的“Kleene 1星”元符号 (*) 用于指示其左侧的符号重复零次或多次。■

Here the “Kleene1 star” metasymbol (*) is used to indicate zero or more repetitions of the symbol to its left. ■

当然,数字只是符号:纸上的墨迹或屏幕上的像素。它们本身没有任何意义。当我们说数字代表数学家定义的从零到九的自然数时,我们为数字添加了语义。或者,我们可以说它们代表颜色,或十进制日历中的星期几。这些将构成相同语法的替代语义。以类似的方式,我们通过将十进制、位值解释与每个自然数相关联来定义自然数的语义数字串。可以为有理数、(有限精度)实数、算术、赋值、控制流、声明以及实际上所有编程语言设计类似的语法规则和语义解释。

Of course, digits are only symbols: ink blobs on paper or pixels on a screen. They carry no meaning in and of themselves. We add semantics to digits when we say that they represent the natural numbers from zero to nine, as defined by mathematicians. Alternatively, we could say that they represent colors, or the days of the week in a decimal calendar. These would constitute alternative semantics for the same syntax. In a similar fashion, we define the semantics of natural numbers by associating a base-10, place-value interpretation with each string of digits. Similar syntax rules and semantic interpretations can be devised for rational numbers, (limited-precision) real numbers, arithmetic, assignments, control flow, declarations, and indeed all of programming languages.

区分语法和语义至少有两个好处。首先,不同的编程语言通常提供语义非常相似但语法非常不同的功能。如果能够识别不熟悉的语法下的常见(并且可能很熟悉)语义思想,学习一门新语言通常会容易得多。其次,编译器或解释器可以使用一些非常高效和优雅的算法来发现计算机程序的句法结构(但不是语义!),这些算法可用于驱动其余的编译或解释过程。

Distinguishing between syntax and semantics is useful for at least two reasons. First, different programming languages often provide features with very similar semantics but very different syntax. It is generally much easier to learn a new language if one is able to identify the common (and presumably familiar) semantic ideas beneath the unfamiliar syntax. Second, there are some very efficient and elegant algorithms that a compiler or interpreter can use to discover the syntactic structure (but not the semantics!) of a computer program, and these algorithms can be used to drive the rest of the compilation or interpretation process.

在本章中,我们将重点关注语法:如何指定编程语言的结构规则,以及编译器如何识别给定输入程序的结构。 这两个任务(指定语法规则和弄清给定程序是如何(以及是否)根据这些规则构建的)是不同的。 第一个任务主要与想要编写有效程序的程序员有关。 第二个任务主要与需要分析这些程序的编译器有关。 第一项任务依赖于正则表达式上下文无关语法,它们指定如何生成有效程序。 第二项任务依赖于扫描器解析器,它们识别程序结构。 我们在第 2.1 节中讨论第一个任务,在第 2.22.3节中讨论第二个任务。

In the current chapter we focus on syntax: how we specify the structural rules of a programming language, and how a compiler identifies the structure of a given input program. These two tasks—specifying syntax rules and figuring out how (and whether) a given program was built according to those rules—are distinct. The first is of interest mainly to programmers, who want to write valid programs. The second is of interest mainly to compilers, which need to analyze those programs. The first task relies on regular expressions and context-free grammars, which specify how to generate valid programs. The second task relies on scanners and parsers, which recognize program structure. We address the first of these tasks in Section 2.1, the second in Sections 2.2 and 2.3.

在第 2.4 节(主要在配套网站上)中,我们深入研究了扫描和解析的基本理论。从理论角度来说,扫描器是确定性有限自动机(DFA),可识别编程语言的标记。解析器是确定性下推自动机(PDA),可识别语言的上下文无关语法。事实证明,人们可以从正则表达式和上下文无关语法自动生成扫描器和解析器。此任务由 Unix 的lexyacc 2等工具执行。在计算机科学中,可能没有其他地方如此清晰、如此令人信服地将理论与实践联系起来。

In Section 2.4 (largely on the companion site) we take a deeper look at the formal theory underlying scanning and parsing. In theoretical parlance, a scanner is a deterministic finite automaton (DFA) that recognizes the tokens of a programming language. A parser is a deterministic push-down automaton (PDA) that recognizes the language's context-free syntax. It turns out that one can generate scanners and parsers automatically from regular expressions and context-free grammars. This task is performedbytools like Unix's lex and yacc,2 among others. Possibly nowhere else in computer science is the connection between theory and practice so clear and so compelling.

2.1 指定语法:正则表达式和上下文无关文法

2.1 Specifying Syntax: Regular Expressions and Context-Free Grammars

语法的正式规范需要一套规则。语法的复杂程度(表达能力)取决于我们可以使用哪种规则。事实证明,我们直观地认为的标记可以用三种正式规则从单个字符构造出来:连接、交替(在一组有限的备选方案中进行选择)和所谓的“克莱尼闭包”(重复任意次数)。指定我们直观认为的语法的其余大部分内容需要一种额外的规则:递归(从相同构造的更简单实例创建构造)。任何可以根据前三个规则定义的字符串集合称为正则集,有时也称为正则语言。正则集由正则表达式生成并被扫描器识别。任何可以通过添加递归来定义的字符串集合称为上下文无关语言(CFL)。上下文无关语言由上下文无关语法(CFG)生成并被解析器识别。(这里的术语可能令人困惑。“语言”一词的含义差异很大,取决于我们谈论的是“形式”语言 [例如,正则语言或上下文无关语言] 还是编程语言。形式语言只是一组字符串,没有伴随的语义。)

Formal specification of syntax requires a set of rules. How complicated (expressive) the syntax can be depends on the kinds of rules we are allowed to use. It turns out that what we intuitively think of as tokens can be constructed from individual characters using just three kinds of formal rules: concatenation, alternation (choice among a finite set of alternatives), and so-called “Kleene closure” (repetition an arbitrary number of times). Specifying most of the rest of what we intuitively think of as syntax requires one additional kind of rule: recursion (creation of a construct from simpler instances of the same construct). Any set of strings that can be defined in terms of the first three rules is called a regular set, or sometimes a regular language. Regular sets are generated by regular expressions and recognized by scanners. Any set of strings that can be defined if we add recursion is called a context-free language (CFL). Context-free languages are generated by context-free grammars (CFGs) and recognized by parsers. (Terminology can be confusing here. The meaning of the word “language” varies greatly, depending on whether we're talking about “formal” languages [e.g., regular or context-free], or programming languages. A formal language is just a set of strings, with no accompanying semantics.)

2.1.1 标记和正则表达式

2.1.1 Tokens and Regular Expressions

标记是程序的基本构造块——具有独立含义的最短字符串。标记有多种类型,包括关键字、标识符、符号和各种类型的常量。某些类型的标记(例如,增量运算符)仅对应一串字符。其他类型的标记(例如,标识符)对应于一组具有某种共同形式的字符串。(在大多数语言中,关键字是特殊的字符串,它们具有作为标识符的正确形式,但保留用于特殊用途。)我们将非正式地使用“标记”一词来指代一般类型(标识符、增量运算符)和特定字符串(foo, ++);它们之间的区别应该从上下文中很清楚。

Tokens are the basic building blocks of programs—the shortest strings of characters with individual meaning. Tokens come in many kinds, including keywords, identifiers, symbols, and constants of various types. Some kinds of token (e.g., the increment operator) correspond to only one string of characters. Others (e.g., identifier) correspond to a set of strings that share some common form. (In most languages, keywords are special strings of characters that have the right form to be identifiers, but are reserved for special purposes.) We will use the word “token” informally to refer to both the generic kind (an identifier, the increment operator) and the specific string (foo, ++); the distinction between these should be clear from context.

例 2.2

Example 2.2

C11 的词汇结构

Lexical structure of C11

有些语言只有几种标记,形式相当简单。其他语言则更复杂。例如,C 语言有 100 多种标记,包括 44 个关键字(doubleifreturnstruct等);标识符(my_variableyour_typesizeofprintf等);整数(0765、0x1f5、501)、浮点数(6.022e23)和字符(' x '、' \ '、' \0170 ')常量;字符串文字(“ snerk ”、“ say \” hi \ “\ n ”);54 个“标点符号”(+、]、->、*=、:、|| 等)和两种不同形式的注释。有针对国际字符集、跨多行源代码的字符串文字、不同精度(宽度)的常量、某些输入设备上缺少的符号的替代“拼写”,以及从较小部分构建标记的预处理器宏的规定。其他大型现代语言(Java、Ada)也同样复杂。■

Some languages have only a few kinds of token, of fairly simple form. Other languages are more complex. C, for example, has more than 100 kinds of tokens, including 44 keywords (double, if, return, struct, etc.); identifiers (my_variable, your_type, sizeof, printf, etc.); integer (0765, 0x1f5, 501), floating-point (6.022e23), and character ('x', '\', '\0170') constants; string literals (“snerk“, “say \”hi\ “\n“); 54 “punctuators” (+, ], ->, *=, :, ||, etc.), and two different forms of comments. There are provisions for international character sets, string literals that span multiple lines of source code, constants of varying precision (width), alternative “spellings” for symbols that are missing on certain input devices, and preprocessor macros that build tokens from smaller pieces. Other large, modern languages (Java, Ada) are similarly complex. ■

为了指定标记,我们使用正则表达式的符号。正则表达式是下列之一:

To specify tokens, we use the notation of regular expressions. A regular expression is one of the following:

1. 一个角色

1. A character

2. 空串,记为ε

2. The empty string, denoted ε

3. 两个正则表达式相邻,即第一个正则表达式生成的任意字符串后跟(连接)第二个正则表达式生成的任意字符串

3. Two regular expressions next to each other, meaning any string generated by the first one followed by (concatenated with) any string generated by the second one

4. 两个正则表达式用竖线(|)分隔,表示第一个正则表达式生成的任意字符串,或者第二个正则表达式生成的任意字符串

4. Two regular expressions separated by a vertical bar (|), meaning any string generated by the first one or any string generated by the second one

5. 正则表达式后跟一个 Kleene 星号,表示由星号前面的表达式生成的零个或多个字符串的连接

5. A regular expression followed by a Kleene star, meaning the concatenation of zero or more strings generated by the expression in front of the star

括号用于避免各个子表达式的开始和结束位置产生歧义。3

Parentheses are used to avoid ambiguity about where the various subexpressions start and end.3

例 2.3

Example 2.3

数字常量的语法

Syntax of numeric constants

例如,考虑一下简单的手持计算器接受的数字常量的语法:

Consider, for example, the syntax of numeric constants accepted by a simple hand-held calculator:

数字整数|实数

numberinteger | real

整数数字 数字*

integerdigit digit *

实数整数指数|小数指数| ε

realinteger exponent | decimal ( exponent | ε )

小数数字* (|.数字|数字. )数字*

decimaldigit * (|. digit | digit . ) digit *

指数→ ( e | E ) ( + | − | ε )整数

exponent → ( e | E ) ( + | − | ε ) integer

数字→ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

→ 符号左侧的符号为正则表达式提供名称。其中一个(数字)将用作标记名称;其他的只是以方便构建更大的表达式。4注意,虽然我们允许定义相互构建,但没有任何东西是根据自身定义的,即使是间接的。这种递归定义是上下文无关语法的显著特征,如第2.1.2 节所述。为了生成有效数字,我们展开子定义,然后从左到右扫描结果表达式,在每个垂直条中选择替代方案,并在每个 Kleene 星号处选择一定数量的重复。在每次重复中,我们可能会在垂直条上做出不同的选择,从而生成不同的子字符串。■

The symbols to the left of the → signs provide names for the regular expressions. One of these (number) will serve as a token name; the others are simply for convenience in building larger expressions.4 Note that while we have allowed definitions to build on one another, nothing is ever defined in terms of itself, even indirectly. Such recursive definitions are the distinguishing characteristic of context-free grammars, described in Section 2.1.2. To generate a valid number, we expand out the sub-definitions and then scan the resulting expression from left to right, choosing among alternatives at each vertical bar, and choosing a number of repetitions at each Kleene star. Within each repetition we may make different choices at vertical bars, generating different substrings. ■

设计与实现

Design & Implementation

2.1 上下文关键词

2.1 Contextual keywords

除了区分关键字和标识符之外,某些语言还定义了所谓的上下文关键字,它们在程序中的某些特定位置充当关键字,但在其他地方充当标识符。例如,在 C# 中,单词Yield可以出现在returnbreak之前- 这是标识符永远不能出现的地方。在这种情况下,它被解释为关键字;在其他任何地方它都是标识符。因此,有一个名为Yield 的局部变量是完全可以接受的:编译器可以根据它在程序中出现的位置将其与关键字区分开来。

In addition to distinguishing between keywords and identifiers, some languages define so-called contextual keywords, which function as keywords in certain specific places in a program, but as identifiers elsewhere. In C#, for example, the word yield can appear immediately before return or break—a place where an identifier can never appear. In this context, it is interpreted as a keyword; anywhere else it is an identifier. It is therefore perfectly acceptable to have a local variable named yield: the compiler can distinguish it from the keyword by where it appears in the program.

C++11 有少量上下文关键字。C# 4.0 有 26 个。大多数是在修订语言以创建新标准版本的过程中引入的。鉴于用户群体庞大,任何简短、直观的单词都可能被某些现有程序中的某人用作标识符。在新版本的语言中将该词设为上下文关键字,而不是完整关键字,可以降低现有程序突然无法编译的风险。

C++11 has a small handful of contextual keywords. C# 4.0 has 26. Most were introduced in the course of revising the language to create a new standard version. Given a large user community, any short, intuitively appealing word is likely to have been used as an identifier by someone, in some existing program. Making that word a contextual keyword in the new version of the language, rather than a full keyword, reduces the risk that existing programs will suddenly fail to compile.

字符集和格式问题

Character Sets and Formatting Issues

在某些语言(例如 Perl、Python 和 Ruby;C 及其后代)中,标识符和关键字中的大小写字母被视为不同的,而在其他语言(例如 Ada、Common Lisp 和 Fortran)中则视为相同。因此,fooFooFOO在 Ada 中都表示相同的标识符,但在 C 中则表示不同的标识符。Modula-2 和 Modula-3 要求关键字和预定义(内置)标识符必须大写;C 及其后代要求它们必须小写。少数语言只允许标识符中使用字母和数字。大多数允许使用下划线。少数语言(尤其是 Lisp)允许使用各种其他字符。某些语言(例如 Java 和 C#)对名称中大小写字母的使用有标准(但可选)的约定。5

Upper- and lowercase letters in identifiers and keywords are considered distinct in some languages (e.g., Perl, Python, and Ruby; C and its descendants), and identical in others (e.g., Ada, Common Lisp, and Fortran). Thus foo, Foo, and FOO all represent the same identifier in Ada, but different identifiers in C. Modula-2 and Modula-3 require keywords and predefined (built-in) identifiers to be written in uppercase; C and its descendants require them to be written in lowercase. A few languages allow only letters and digits in identifiers. Most allow underscores. A few (notably Lisp) allow a variety of additional characters. Some languages (e.g., Java and C#) have standard (but optional) conventions on the use of upper- and lowercase letters in names.5

随着计算的全球化,非拉丁字符集变得越来越重要。许多现代语言(包括 C、C++、Ada 95、Java、C# 和 Fortran 2003)都引入了对多字节字符集的明确支持,这些字符集通常基于 Unicode 和 ISO/IEC 10646 国际标准。大多数现代编程语言允许非拉丁字符出现在注释和字符串中;越来越多的语言也允许它们出现在标识符中。跨字符集的可移植性和针对给定字符集的本地化的约定可能非常复杂,特别是当需要各种形式的向后兼容性时(C99 基本原理用整整五页来讨论这个主题 [ Int03a,第 19-23 页]);我们在这里主要忽略这些问题。

With the globalization of computing, non-Latin character sets have become increasingly important. Many modern languages, including C, C++, Ada 95, Java, C#, and Fortran 2003 have introduced explicit support for multibyte character sets, generally based on the Unicode and ISO/IEC 10646 international standards. Most modern programming languages allow non-Latin characters to appear within comments and character strings; an increasing number allow them in identifiers as well. Conventions for portability across character sets and for localization to a given character set can be surprisingly complex, particularly when various forms of backward compatibility are required (the C99 Rationale devotes five full pages to this subject [Int03a, pp. 19–23]); for the most part we ignore such issues here.

某些语言实现对标识符的最大长度施加了限制,但大多数语言都避免了这种不必要的限制。大多数现代语言也或多或少是自由格式的,这意味着程序只是一系列标记:重要的是它们相对于彼此的顺序,而不是它们在打印行或页面中的物理位置。标记之间的“空白”(空格、制表符、回车符以及换行符和分页符)通常会被忽略,除非需要将一个标记与下一个标记分开。

Some language implementations impose limits on the maximum length of identifiers, but most avoid such unnecessary restrictions. Most modern languages are also more or less free format, meaning that a program is simply a sequence of tokens: what matters is their order with respect to one another, not their physical position within a printed line or page. “White space” (blanks, tabs, carriage returns, and line and page feed characters) between tokens is usually ignored, except to the extent that it is needed to separate one token from the next.

这些规则有几个值得注意的例外。一些语言实现限制了一行的最大长度,以允许编译器将当前行存储在固定长度的缓冲区中。Fortran 90 之前的 Fortran 方言使用固定格式,每行 72 个字符(曾经存储程序的打孔卡的宽度),并且行内的不同列保留用于不同用途。在其他几种语言中,换行符用于分隔语句,包括 Go、Haskell、Python 和 Swift。Haskell 和 Python 也赋予缩进特殊的意义。例如,循环主体恰好由缩进比循环头更远的后续行组成。

There are a few noteworthy exceptions to these rules. Some language implementations limit the maximum length of a line, to allow the compiler to store the current line in a fixed-length buffer. Dialects of Fortran prior to Fortran 90 use a fixed format, with 72 characters per line (the width of a paper punch card, on which programs were once stored), and with different columns within the line reserved for different purposes. Line breaks serve to separate statements in several other languages, including Go, Haskell, Python, and Swift. Haskell and Python also give special significance to indentation. The body of a loop, for example, consists of precisely those subsequent lines that are indented farther than the header of the loop.

正则表达式的其他用途

Other Uses of Regular Expressions

许多读者都熟悉Unix 中的grep系列工具、各种文本编辑器的搜索功能或 Perl、Python、Ruby、awksed等脚本语言和工具中的正则表达式。其中大多数都为正则表达式的表示法提供了丰富的扩展。某些扩展(例如“零次或一次出现”或“除空格以外的任何内容”的简写)不会改变表示法的功能。其他扩展(例如,要求在输入字符串的后面出现与表达式的前面部分匹配的相同字符序列的第二次出现)增强了表示法的功能,因此它不再局限于生成正则集。其他扩展的设计目的不是增加表示法的表现力,而是将其与其他语言功能联系起来。例如,在许多工具中,可以将正则表达式的各个部分括起来,这样当一个字符串与其匹配时,相应子字符串的内容就会被分配到命名的局部变量中。我们将在第 14.4.2 节的脚本语言背景下再次讨论这些问题。

Many readers will be familiar with regular expressions from the grep family of tools in Unix, the search facilities of various text editors, or such scripting languages and tools as Perl, Python, Ruby, awk, and sed. Most of these provide a rich set of extensions to the notation of regular expressions. Some extensions, such as shorthand for “zero or one occurrences” or “anything other than white space,” do not change the power of the notation. Others, such as the ability to require a second occurrence, later in the input string, of the same character sequence that matched an earlier part of the expression, increase the power of the notation, so that it is no longer restricted to generating regular sets. Still other extensions are designed not to increase the expressiveness of the notation but rather to tie it to other language facilities. In many tools, for example, one can bracket portions of a regular expression in such a way that when a string is matched against it the contents of the corresponding substrings are assigned into named local variables. We will return to these issues in Section 14.4.2, in the context of scripting languages.

2.1.2 上下文无关文法

2.1.2 Context-Free Grammars

例 2.4

Example 2.4

表达式中的语法嵌套

Syntactic nesting in expressions

正则表达式非常适合定义标记。但是,它们无法指定嵌套结构,而嵌套结构是编程语言的核心。例如,考虑算术表达式的结构:

Regular expressions work well for defining tokens. They are unable, however, to specify nested constructs, which are central to programming languages. Consider for example the structure of an arithmetic expression:

设计与实现

Design & Implementation

2.2 格式限制

2.2 Formatting restrictions

受实现问题启发的格式化限制(如 Fortran 77 及其前身的打卡式规则)往往会随着实现技术的改进而成为不受欢迎的时代错误。鉴于某些文字处理器倾向于“填充”或自动格式化文本,Haskell、Occam 和 Python 等语言的换行和缩进规则有些争议。

Formatting limitations inspired by implementation concerns—as in the punch-card-oriented rules of Fortran 77 and its predecessors—have a tendency to become unwanted anachronisms as implementation techniques improve. Given the tendency of certain word processors to “fill” or auto-format text, the line break and indentation rules of languages like Haskell, Occam, and Python are somewhat controversial.

expr → id | number | − expr | ( expr ) | expr op expr

expr → id | number | − expr | ( expr ) | expr op expr

操作→ + | − | * | /

op → + | − | * | /

在这里,根据自身定义构造的能力至关重要。除其他外,它允许我们确保左括号和右括号匹配,这是正则表达式无法实现的(有关更多详细信息,请参阅第 C-2.4.3 节)。箭头符号 (→) 表示“可以具有形式”;为简洁起见,有时将其发音为“goes to”。■

Here the ability to define a construct in terms of itself is crucial. Among other things, it allows us to ensure that left and right parentheses are matched, something that cannot be accomplished with regular expressions (see Section C-2.4.3 for more details). The arrow symbol (→) means “can have the form”; for brevity it is sometimes pronounced “goes to.” ■

上下文无关文法中的每个规则称为一个产生式。产生式左侧的符号称为变量或非终结符。可以有任意数量的产生式具有相同的左侧。组成从文法得出的字符串的符号称为终结符(此处以打字机字体显示)。它们不能出现在任何产生式的左侧。在编程语言中,上下文无关文法的终结符是该语言的标记。其中一个非终结符(通常是第一个产生式左侧的非终结符)称为起始符号它命名由整个文法定义的构造。

Each of the rules in a context-free grammar is known as a production. The symbols on the left-hand sides of the productions are known as variables, or nonterminals. There may be any number of productions with the same left-hand side. Symbols that are to make up the strings derived from the grammar are known as terminals (shown here in typewriter font). They cannot appear on the left-hand side of any production. In a programming language, the terminals of the context-free grammar are the language's tokens. One of the nonterminals, usually the one on the left-hand side of the first production, is called the start symbol. It names the construct defined by the overall grammar.

例 2.5

Example 2.5

扩展 BNF(EBNF)

Extended BNF (EBNF)

上下文无关文法的表示法有时被称为巴科斯范式 (BNF),以纪念 John Backus 和 Peter Naur,他们为 Algol-60 编程语言 [ NBB + 63 ] 的定义设计了这种表示法。6严格来说,正则表达式的 Kleene 星号和元级括号在 BNF 中是不允许的,但它们不会改变表示法的表达能力,并且通常为了方便而包括在内。有时还会看到“Kleene 加号”( + );它表示前面的符号或符号组的一个或多个实例。7使用这些额外的运算符进行扩充时,该表示法通常称为扩展 BNF (EBNF)。构造

The notation for context-free grammars is sometimes called Backus-Naur Form (BNF), in honor of John Backus and Peter Naur, who devised it for the definition of the Algol-60 programming language [NBB+63].6 Strictly speaking, the Kleene star and meta-level parentheses of regular expressions are not allowed in BNF, but they do not change the expressive power of the notation, and are commonly included for convenience. Sometimes one sees a “Kleene plus” (+) as well; it indicates one or more instances of the symbol or group of symbols in front of it.7 When augmented with these extra operators, the notation is often called extended BNF (EBNF). The construct

id_list → id (, id)*

id_list → id (, id)*

是简写

is shorthand for

id_list → id

id_list → id

id_列表id_列表, id

id_listid_list, id

“Kleene plus” 类似。请注意,这里的括号是元符号。在示例 2.4中,它们是所定义语言的一部分,并且以等宽字体书写。8

“Kleene plus” is analogous. Note that the parentheses here are metasymbols. In Example 2.4 they were part of the language being defined, and were written in fixed-width font.8

就像 Kleene 星号和括号一样,竖线在某种意义上是多余的,尽管它在原始 BNF 中是提供的。构造

Like the Kleene star and parentheses, the vertical bar is in some sense superfluous, though it was provided in the original BNF. The construct

操作→ + | − | * | /

op → + | − | * | /

可以被认为是

can be considered shorthand for

操作→ +

op → +

操作→ −

op → −

操作→ *

op → *

操作→ /

op → /

有时也写作

which is also sometimes written

操作→ +

op → +

 → −

 → −

 → *

 → *

 → /

 → /

许多标记(例如上面的idnumber)有多种可能的拼写(即,可能由多种可能的字符串表示)。解析器对这些不知情;它不会区分一个标识符与另一个标识符。然而,语义分析器会区分它们;扫描器必须保存每个这样的“有趣”标记的拼写以供以后使用。

Many tokens, such as id and number above, have many possible spellings (i.e., may be represented by many possible strings of characters). The parser is oblivious to these; it does not distinguish one identifier from another. The semantic analyzer does distinguish them, however; the scanner must save the spelling of each such “interesting” token for later use.

2.1.3 推导和解析树

2.1.3 Derivations and Parse Trees

上下文无关语法向我们展示了如何生成语法上有效的终结符字符串:从起始符号开始。选择一个起始符号在左侧的产生式;用该产生式的右侧替换起始符号。现在在结果字符串中选择一个非终结符A ,选择一个左侧有A的产生式P ,用P的右侧替换A。重复此过程,直到没有非终结符剩余。

A context-free grammar shows us how to generate a syntactically valid string of terminals: Begin with the start symbol. Choose a production with the start symbol on the left-hand side; replace the start symbol with the right-hand side of that production. Now choose a nonterminal A in the resulting string, choose a production P with A on its left-hand side, and replace A with the right-hand side of P. Repeat this process until no nonterminals remain.

例 2.6

Example 2.6

斜率 * x + 截距的推导

Derivation of slope * x + intercept

举个例子,我们可以使用表达式语法来生成字符串“slope * x +略微截距”

As an example, we can use our grammar for expressions to generate the string “slope * x + intercept”:

exprexpr op expr

exprexpr op expr

 expr op id

 expr op id

 表达式+ id

 expr + id

 expr op expr + id

 expr op expr + id

 expr op id + id

 expr op id + id

 表达式* id + id

 expr * id + id

 ⇒ id(斜率)* id(x)+ id(截距)

 ⇒ id (slope) * id (x) + id (intercept)

元符号通常发音为“derives”。它表示右侧是通过使用产生式替换左侧的一些非终结符而得到的。在每一行中,我们都用下划线标出了在下一行中被替换的符号A。 ■

The metasymbol is often pronounced “derives.” It indicates that the right-hand side was obtained by using a production to replace some nonterminal in the left-hand side. At each line we have underlined the symbol A that is replaced in the following line. ■

一系列替换操作,展示如何从起始符号导出一串终结符,称为一次推导。沿途的每一串符号称为一个句型。最终的句型仅由终结符组成,称为推导的产物。我们有时会省略中间步骤,写为expr ⇒* slope * x + interval,其中元符号⇒*表示“在零次或多次替换后导出”。在这个特定的推导中,我们在每个步骤都选择用某个产生式的右边替换最右边的非终结符。这种替换策略导致最右边的推导。还有许多其他可能的推导,包括最左边的推导和介于两者之间的选项。

A series of replacement operations that shows how to derive a string of terminals from the start symbol is called a derivation. Each string of symbols along the way is called a sentential form. The final sentential form, consisting of only terminals, is called the yield of the derivation. We sometimes elide the intermediate steps and write expr ⇒* slope * x + intercept, where the metasymbol ⇒* means “derives after zero or more replacements.” In this particular derivation, we have chosen at each step to replace the right-most nonterminal with the right-hand side of some production. This replacement strategy leads to a right-most derivation. There are many other possible derivations, including left-most and options in-between.

我们在第 1 章中看到,我们可以用解析树的形式来表示推导。解析树的根是语法的起始符号。树的叶子是其收益。每个内部节点及其子节点都表示产生式的使用。

We saw in Chapter 1 that we can represent a derivation graphically as a parse tree. The root of the parse tree is the start symbol of the grammar. The leaves of the tree are its yield. Each internal node, together with its children, represents the use of a production.

例 2.7

Example 2.7

解析斜率 * x + 截距的树

Parse trees for slope * x + intercept

图 2.1显示了示例表达式的解析树。这棵树并不唯一。在树的第二层,我们可以选择将运算符变成 * 而不是 +,并进一步扩展右侧的表达式,而不是左侧的表达式(参见图 2.2)。允许为一些终端字符串构建多于一棵解析树被称为是歧义的。歧义在尝试构建解析器时会成为一个问题:它需要一些额外的机制来推动在同样可接受的替代方案之间做出选择。■

A parse tree for our example expression appears in Figure 2.1. This tree is not unique. At the second level of the tree, we could have chosen to turn the operator into a * instead of a +, and to further expand the expression on the right, rather than the one on the left (see Figure 2.2). A grammar that allows the construction of more than one parse tree for some string of terminals is said to be ambiguous. Ambiguity turns out to be a problem when trying to build a parser: it requires some extra mechanism to drive a choice between equally acceptable alternatives. ■

编号02-01-9780124104099
图 2.1 解析斜率 * x + 截距的树(示例 2.4中的语法)。
传真:02-02-9780124104099
图 2.2 斜率 * x + 截距的替代(不太理想)解析树(示例 2.4中的语法)。存在多棵树这一事实意味着我们的语法是模棱两可的。

稍加思考就会发现,对于任何给定的上下文无关语言,都有无数个上下文无关语法。9但是,有些语法比其他语法有用得多。在本文中,我们将避免使用歧义语法(尽管大多数解析器生成器都允许使用它们,方法是使用消歧规则)。我们还将避免使用所谓的无用符号:不能生成任何终结符串的非终结符,或不能出现在任何派生结果中的终结符。

A moment's reflection will reveal that there are infinitely many context-free grammars for any given context-free language.9 Some grammars, however, are much more useful than others. In this text we will avoid the use of ambiguous grammars (though most parser generators allow them, by means of disambiguating rules). We will also avoid the use of so-called useless symbols: nonterminals that cannot generate any string of terminals, or terminals that cannot appear in the yield of any derivation.

在为一种编程语言设计文法时,我们一般会尝试找到一种能够反映程序内部结构的文法,以便编译器的其余部分能够使用它。(我们将在2.3.2 节中看到,我们还会尝试找到一种能够高效解析的文法,这可能有点困难。)结构在算术表达式中尤为重要,我们可以使用产生式来捕获各种运算符的结合性优先级。结合性告诉我们,大多数语言中的运算符都是从左到右分组的,因此10 − 4 − 3表示(10 − 4) − 3 ,而不是10 − (4 − 3)。优先级告诉我们,大多数语言中的乘法和除法比加法和减法分组更紧密,因此3 + 4 * 5表示3 + (4 * 5),而不是(3 + 4) * 5。 (这些规则并不通用;我们将在第 6.1.1 节中再次考虑它们。)

When designing the grammar for a programming language, we generally try to find one that reflects the internal structure of programs in a way that is useful to the rest of the compiler. (We shall see in Section 2.3.2 that we also try to find one that can be parsed efficiently, which can be a bit of a challenge.) One place in which structure is particularly important is in arithmetic expressions, where we can use productions to capture the associativity and precedence of the various operators. Associativity tells us that the operators in most languages group left to right, so that 10 − 4 − 3 means (10 − 4) − 3 rather than 10 − (4 − 3). Precedence tells us that multiplication and division in most languages group more tightly than addition and subtraction, so that 3 + 4 * 5 means 3 + (4 * 5) rather than (3 + 4) * 5. (These rules are not universal; we will consider them again in Section 6.1.1.)

例 2.8

Example 2.8

具有优先级和结合性的表达式语法

Expression grammar with precedence and associativity

以下是我们的表达语法的更好版本:

Here is a better version of our expression grammar:

1. exprterm | expr add_op term

1. exprterm | expr add_op term

2.术语因子|术语 mult_op 因子

2. termfactor | term mult_op factor

3.因子→ id | 数字 | −因子| ( expr )

3. factor → id | number | − factor | ( expr )

4.添加操作→ + | −

4. add_op → + | −

5.多操作→ * | /

5. mult_op → * | /

这个文法是明确的。它以factor、termexpr相互构建的方式捕获优先级,每个级别上都有不同的运算符。它在第 1 行和第 2 行的后半部分捕获结合性,即将 sub expr和 sub term构建在运算符的左侧,而不是右侧。在图 2.3中,我们可以看到如何在文法中构建优先级概念,从而清楚地表明在3 + 4 * 5中乘法的分组比加法更紧密,即使没有括号也是如此。在图 2.4中,我们可以看到减法更紧密地向左分组,因此10 − 4 − 3的计算结果为3,而不是9。

This grammar is unambiguous. It captures precedence in the way factor, term, and expr build on one another, with different operators appearing at each level. It captures associativity in the second halves of lines 1 and 2, which build subexprs and sub terms to the left of the operator, rather than to the right. In Figure 2.3, we can see how building the notion of precedence into the grammar makes it clear that multiplication groups more tightly than addition in 3 + 4 * 5, even without parentheses. In Figure 2.4, we can see that subtraction groups more tightly to the left, so that 10 − 4 − 3 would evaluate to 3, rather than to 9. ■

传真:02-03-9780124104099
图 2.3 3 + 4 * 5 的解析树,具有优先权(示例 2.8中的语法)。
传真:02-04-9780124104099
图 2.4 10 − 4 − 3 的分析树,具有左结合性(示例 2.8中的语法)。

02-01-9780124104099检查你的理解

Check Your Understanding

1. 句法和语义有什么区别?

1. What is the difference between syntax and semantics?

2. 可以使用哪三种基本运算从较简单的正则表达式构建复杂的正则表达式?

2. What are the three basic operations that can be used to build complex regular expressions from simpler regular expressions?

3. 上下文无关文法除了正则表达式的三种操作之外,还提供了哪些附加操作?

3. What additional operation (beyond the three of regular expressions) is provided in context-free grammars?

4. 什么是Backus-Naur 范式?它是何时以及为何被发明的?

4. What is Backus-Naur form? When and why was it devised?

5. 说出一种缩进影响程序语法的语言。

5. Name a language in which indentation affects program syntax.

6. 在讨论上下文无关语言时,什么是派生?什么是句子形式?

6. When discussing context-free languages, what is a derivation? What is a sentential form?

7. 最右推导和最左推导有什么区别?

7. What is the difference between a right-most derivation and a left-most derivation?

8. 上下文无关文法具有歧义是什么意思?

8. What does it mean for a context-free grammar to be ambiguous?

9. 什么是结合性优先级?为什么它们在解析树中很重要?

9. What are associativity and precedence? Why are they significant in parse trees?

2.2 扫描

2.2 Scanning

编程语言的扫描器和解析器共同负责发现程序的语法结构。这个发现过程或语法分析是将程序翻译成目标语言的等效程序的必要的第一步。(这也是直接解释程序的第一步。一般来说,在本书的其余部分,我们将重点关注编译,而不是解释。我们将要讨论的大部分内容要么明显适用于解释,要么显然与解释无关。)

Together, the scanner and parser for a programming language are responsible for discovering the syntactic structure of a program. This process of discovery, or syntax analysis, is a necessary first step toward translating the program into an equivalent program in the target language. (It's also the first step toward interpreting the program directly. In general, we will focus on compilation, rather than interpretation, for the remainder of the book. Most of what we shall discuss either has an obvious application to interpretation, or is obviously irrelevant to it.)

通过将输入字符分组为标记,扫描器大大减少了计算量更大的解析器必须检查的单个项目的数量。此外,扫描器通常会删除注释(因此解析器不必担心它们出现在整个上下文无关语法中 - 参见练习 2.20);保存“有趣”标记的文本,如标识符、字符串和数字文字;并使用行号和列号标记标记,以便在后续阶段更轻松地生成高质量的错误消息。

By grouping input characters into tokens, the scanner dramatically reduces the number of individual items that must be inspected by the more computationally intensive parser. In addition, the scanner typically removes comments (so the parser doesn't have to worry about them appearing throughout the context-free grammar—see Exercise 2.20); saves the text of “interesting” tokens like identifiers, strings, and numeric literals; and tags tokens with line and column numbers, to make it easier to generate high-quality error messages in subsequent phases.

例 2.9

Example 2.9

计算器语言的标记

Tokens for a calculator language

示例 2.42.8中,我们考虑了一种用于算术表达式的简单语言。在2.3.1 节中,我们将扩展它以创建一种具有输入、输出、变量和赋值的简单“计算器语言”。对于这种语言,我们将使用以下一组标记:

In Examples 2.4 and 2.8 we considered a simple language for arithmetic expressions. In Section 2.3.1 we will extend this to create a simple “calculator language” with input, output, variables, and assignment. For this language we will use the following set of tokens:

赋值→ :=

assign → :=

加号→ +

plus → +

→ −

minus → −

→ *

times → *

div → /

div → /

括号→ (

lparen → (

括号→ )

rparen → )

id字母(字母|数字)* 读写除外

idletter ( letter | digit )* except for read and write

数字数字 数字* |数字* (.数字|数字. )数字*

numberdigit digit * | digit * ( . digit | digit . ) digit *

为了与 Algol 及其后代保持一致(并与 C 系列语言形成对比),我们使用 := 而不是 = 进行赋值。为简单起见,我们省略了示例 2.3中的指数符号。我们还列出了readwrite标记作为id规则的例外(有关此内容的更多信息,请参阅第 2.2.2 节)。为了让扫描器的任务更切合实际,我们借用了 C 中的两种注释样式:

In keeping with Algol and its descendants (and in contrast to the C-family languages), we have used := rather than = for assignment. For simplicity, we have omitted the exponential notation found in Example 2.3. We have also listed the tokens read and write as exceptions to the rule for id (more on this in Section 2.2.2). To make the task of the scanner a little more realistic, we borrow the two styles of comment from C:

注释→ /* (-* | *-/ )* * + / | // (非换行符)*换行符

comment → /* ( non-* | * non-/ )* *+ / | // ( non-newline )* newline

这里我们分别使用了-*、-/ 和非换行符作为除 *、/ 和换行符之外的所有字符的简写。■

Here we have used non-*, non-/, and non-newline as shorthand for the alternation of all characters other than *, /, and newline, respectively. ■

例 2.10

Example 2.10

计算器令牌的临时扫描器

An ad hoc scanner for calculator tokens

我们如何识别计算器语言的标记?最简单的方法是完全临时的。伪代码如图2.5所示。我们可以按照自己喜欢的方式构造代码,但首先检查更简单、更常见的情况,在需要时提前查看,并为注释和标识符和数字等长标记嵌入循环似乎是合理的。

How might we go about recognizing the tokens of our calculator language? The simplest approach is entirely ad hoc. Pseudocode appears in Figure 2.5. We can structure the code however we like, but it seems reasonable to check the simpler and more common cases first, to peek ahead when we need to, and to embed loops for comments and for long tokens such as identifiers and numbers.

传真:02-05-9780124104099
图 2.5我们计算器语言中的标记临时扫描器的概要。

找到标记后,扫描器返回到解析器。再次调用时,它会从头开始重复该算法,使用下一个可用的输入字符(包括上次查看但未使用的字符)。■

After finding a token the scanner returns to the parser. When invoked again it repeats the algorithm from the beginning, using the next available characters of input (including any that were peeked at but not consumed the last time). ■

通常,我们在每次调用扫描器时都接受最长的标记。因此foobar始终是foobar,而永远不会是ffoo或 f oob。更重要的是,在 C 语言等语言中,3.14159是实数,而永远不会是3、 . 和14159。空格(空格、制表符、换行符、注释)通常会被忽略,除非它分隔标记(例如,foo bar与foobar不同)。

As a rule, we accept the longest possible token in each invocation of the scanner. Thus foobar is always foobar and never f or foo or foob. More to the point, in a language like C, 3.14159 is a real number and never 3, ., and 14159. White space (blanks, tabs, newlines, comments) is generally ignored, except to the extent that it separates tokens (e.g., foo bar is different from foobar).

图 2.5可以相当容易地扩展以概述某些较大编程语言的扫描器。然后可以手工充实结果以创建某些实现语言的代码。生产编译器通常使用这种临时扫描器;代码快速而紧凑。然而,在语言开发过程中,通常最好以更结构化的方式构建扫描器,作为有限自动机的显式表示。有限自动机可以从一组正则表达式自动生成,从而当标记定义发生变化时可以轻松地重新生成扫描器。

Figure 2.5 could be extended fairly easily to outline a scanner for some larger programming language. The result could then be fleshed out, by hand, to create code in some implementation language. Production compilers often use such ad hoc scanners; the code is fast and compact. During language development, however, it is usually preferable to build a scanner in a more structured way, as an explicit representation of a finite automaton. Finite automata can be generated automatically from a set of regular expressions, making it easy to regenerate a scanner when token definitions change.

例 2.11

Example 2.11

计算器扫描仪的有限自动机

Finite automaton for a calculator scanner

图 2.6以图形形式显示了我们的计算器语言的标记自动机。自动机以独特的初始状态开始。然后,它根据下一个可用的输入字符从一个状态移动到另一个状态。当它到达指定的一组最终状态之一时,它会识别与该状态关联的标记。“最长可能的标记”规则意味着,只有当下一个字符不能用于继续当前标记时,扫描器才会返回到解析器。■

An automaton for the tokens of our calculator language appears in pictorial form in Figure 2.6. The automaton starts in a distinguished initial state. It then moves from state to state based on the next available character of input. When it reaches one of a designated set of final states it recognizes the token associated with that state. The “longest possible token” rule means that the scanner returns to the parser only when the next character cannot be used to continue the current token. ■

传真:02-06-9780124104099
图 2.6 计算器标记扫描器的图形表示,采用有限自动机的形式。该图与图 2.5中的代码大致相似。图 2.12中已对状态进行了编号以供参考。每个标记的扫描从标记为“开始”的状态开始。识别标记的最终状态用双圆圈表示。注释在被识别后,将扫描器发送回其起始状态,而不是最终状态。

设计与实现

Design & Implementation

2.3 嵌套注释

2.3 Nested comments

嵌套注释对程序员来说非常方便(例如,用于临时“注释掉”大块代码)。但是,扫描器通常只处理非递归构造,因此嵌套注释需要特殊处理。有些语言不允许使用嵌套注释。其他语言则要求语言实现者使用专用注释处理代码来扩充扫描器。C 和 C++ 达成了妥协:/* … */ 样式注释不允许嵌套,但 /* … */ 和 // … 样式注释可以相互嵌套。因此,程序员可以将一种样式用于“普通”注释,将另一种样式用于“注释掉”。(但是,C99 设计者指出,条件编译 ( #if ) 更可取 [ Int03a,第 58 页]。)

Nested comments can be handy for the programmer (e.g., for temporarily “commenting out” large blocks of code). Scanners normally deal only with nonrecursive constructs, however, so nested comments require special treatment. Some languages disallow them. Others require the language implementor to augment the scanner with special-purpose comment-handling code. C and C++ strike a compromise: /* … */ style comments are not allowed to nest, but /* … */ and // … style comments can appear inside each other. The programmer can thus use one style for “normal” comments and the other for “commenting out.” (The C99 designers note, however, that conditional compilation (#if) is preferable [Int03a, p. 58].)

2.2.1 生成有限自动机

2.2.1 Generating a Finite Automaton

虽然有限自动机原则上可以手写,但更常见的是使用扫描器生成器工具从一组正则表达式自动构建一个。对于我们的计算器语言,我们希望将示例 2.9中的正则表达式转换为图 2.6中的自动机。该自动机具有理想的属性,即其动作是确定性的:在任何给定状态下,对于给定的输入字符,永远不会有超过一个可能的输出转换(箭头)由该字符标记。然而事实证明,没有明显的一步算法可以将一组正则表达式转换为等效的确定性有限自动机 (DFA)。典型的扫描器生成器将转换作为一系列三个独立的步骤来实现。

While a finite automaton can in principle be written by hand, it is more common to build one automatically from a set of regular expressions, using a scanner generator tool. For our calculator language, we should like to covert the regular expressions of Example 2.9 into the automaton of Figure 2.6. That automaton has the desirable property that its actions are deterministic: in any given state with a given input character there is never more than one possible outgoing transition (arrow) labeled by that character. As it turns out, however, there is no obvious one-step algorithm to convert a set of regular expressions into an equivalent deterministic finite automaton (DFA). The typical scanner generator implements the conversion as a series of three separate steps.

第一步将正则表达式转换为非确定性有限自动机 (NFA)。NFA 与 DFA 类似,不同之处在于 (1) 给定字符标记的给定状态可能存在多个转换,以及 (2) 可能存在所谓的epsilon 转换:箭头用空字符串符号标记,例如。如果存在从起始状态到终止状态的路径,并且该路径的非 epsilon 转换按顺序用标记的字符标记,则称 NFA 接受输入字符串(标记)。

The first step converts the regular expressions into a nondeterministic finite automaton (NFA). An NFA is like a DFA except that (1) there maybe more than one transition out of a given state labeled by a given character, and (2) there may be so-called epsilon transitions: arrows labeled by the empty string symbol, e. The NFA is said to accept an input string (token) if there exists a path from the start state to a final state whose non-epsilon transitions are labeled, in order, by the characters of the token.

为了避免搜索所有可能的路径以找到“有效”的路径,扫描器生成器的第二步是将 NFA 转换为等效的 DFA:一个接受相同语言的自动机,但其中没有 epsilon 转换,并且没有带有多个由相同字符标记的传出转换的状态。第三步是空间优化,生成具有尽可能少的状态数的最终 DFA。

To avoid the need to search all possible paths for one that “works,” the second step of a scanner generator translates the NFA into an equivalent DFA: an automaton that accepts the same language, but in which there are no epsilon transitions, and no states with more than one outgoing transition labeled by the same character. The third step is a space optimization that generates a final DFA with the minimum possible number of states.

从正则表达式到 NFA

From a Regular Expression to an NFA

例 2.12

Example 2.12

为给定的正则表达式构建 NFA

Constructing an NFA for given regular expression

一个由单个字符c组成的平凡正则表达式等价于一个简单的二状态 NFA(实际上是一个 DFA),如图2.7的 (a) 部分所示。类似地,正则表达式ε等价于一个二状态 NFA,其弧标记为ε。从这个基础开始,我们可以使用三个子构造(如图 2.7 的 (b) 到 (d) 部分所示)来构建更大的 NFA 来表示由较小 NFA 表示的正则表达式的连接、交替或 Kleene 闭包。每一步都保留三个不变量:没有进入初始状态的转换,有一个最终状态,并且没有从最终状态出来的转换。这些不变量允许将较小的自动机连接到较大的自动机中,而不会对在何处创建连接产生任何歧义,也不会创建任何意外路径。■

A trivial regular expression consisting of a single character c is equivalent to a simple two-state NFA (in fact, a DFA), illustrated in part (a) of Figure 2.7. Similarly, the regular expression ε is equivalent to a two-state NFA whose arc is labeled by ε. Starting with this base we can use three subconstructions, illustrated in parts (b) through (d) of the same figure, to build larger NFAs to represent the concatenation, alternation, or Kleene closure of the regular expressions represented by smaller NFAs. Each step preserves three invariants: there are no transitions into the initial state, there is a single final state, and there are no transitions out of the final state. These invariants allow smaller automata to be joined into larger ones without any ambiguity about where to create the connections, and without creating any unexpected paths. ■

传真:02-07-9780124104099
图 2.7 构造与给定正则表达式等价的 NFA。部分 (a) 显示了基本情况:单个字母 c 的自动机。部分 (b)、(c) 和 (d) 分别显示了连接、交替和 Kleene 闭包的构造。每个构造都保留唯一的起始状态和单个终止状态。内部细节隐藏在菱形中心区域中。

例 2.13

Example 2.13

d * ( . d | d . ) d *的 DFA

DFA for d*( .d | d. ) d*

为了使这些构造具体化,我们考虑一个小但并不平凡的例子——示例 2.3中的十进制字符串。它们由包含单个小数点的十进制数字字符串组成。由于只有一位数字,因此小数点可以位于开头或结尾:( . d | d . ),其中为简洁起见,我们使用d来表示任何十进制数字。然后可以在开头或结尾添加任意数量的数字:d * ( . d | d . ) d *。从这个正则表达式开始,并使用图 2.7的构造,我们在图 2.8中说明了等效 NFA 的构造。■

To make these constructions concrete, we consider a small but nontrivial example—the decimal strings of Example 2.3. These consist of a string of decimal digits containing a single decimal point. With only one digit, the point can come at the beginning or the end: ( .d | d. ), where for brevity we use d to represent any decimal digit. Arbitrary numbers of digits can then be added at the beginning or the end: d* ( .d | d. ) d*. Starting with this regular expression and using the constructions of Figure 2.7, we illustrate the construction of an equivalent NFA in Figure 2.8. ■

传真:02-08-9780124104099
图 2.8 构造与正则表达式d * ( . d | d . ) d *等价的 NFA。顶行是 . 和d的原始自动机,以及d *的 Kleene 闭包构造。在第二行和第三行中,我们使用了连接和交替构造来构建 . dd . 和 ( . d | d . )。第四行再次使用连接来完成 NFA。我们已标记最终自动机中的状态,以便在后续图中引用。

从 NFA 到 DFA

From an NFA to a DFA

例 2.14

Example 2.14

d * (. d | d . ) d *的 DFA

DFA for d* ( .d | d. ) d*

由于无法“猜测”从任何给定状态采取的正确转换,任何实际的 NFA 实现都需要探索所有可能的转换,同时或通过回溯。为了避免这种复杂且耗时的策略,我们可以使用“子集”构造将 NFA 转换为等效的 DFA。关键思想是让 DFA 在读取给定输入后的状态表示 NFA在相同输入上可能达到的状态。我们使用图 2.8 中的 NFA说明了2.9中的构造。最初,在它使用任何输入之前,NFA 可能处于状态 1,或者它可能会进行到状态 2、4、5 或 8 的 epsilon 转换。因此,我们为我们的 DFA 创建一个初始状态A来表示这个集合。在输入d时,我们的 NFA 可能会从状态 2 移动到状态 3,或者从状态 8 移动到状态 9。它在这个输入上没有从A中的任何状态进行的其他转换。但是,从状态 3 开始,NFA 可以通过 epsilon 转换到状态 2、4、5 或 8 中的任意一个。因此,我们创建 DFA 状态B,如图所示。

With no way to “guess” the right transition to take from any given state, any practical implementation of an NFA would need to explore all possible transitions, concurrently or via backtracking. To avoid such a complex and time-consuming strategy, we can use a “set of subsets” construction to transform the NFA into an equivalent DFA. The key idea is for the state of the DFA after reading a given input to represent the set of states that the NFA might have reached on the same input. We illustrate the construction in Figure 2.9 using the NFA from Figure 2.8. Initially, before it consumes any input, the NFA maybe in State 1, or it may make epsilon transitions to States 2, 4, 5, or 8. We thus create an initial State A for our DFA to represent this set. On an input of d, our NFA may move from State 2 to State 3, or from State 8 to State 9. It has no other transitions on this input from any of the states in A. From State 3, however, the NFA may make epsilon transitions to any of States 2, 4, 5, or 8. We therefore create DFA State B as shown.

传真:02-09-9780124104099
图 2.9与 图 2.8底部的 NFA 等价的 DFA。DFA 的每个状态表示NFA 在看到相同输入后可能处于的状态集

在 上,我们的 NFA 可以从状态 5 移动到状态 6。此输入上没有来自A中任何状态的其他转换,并且没有来自状态 6 的 epsilon 转换。因此,我们创建单例 DFA 状态C ,如图所示。状态ABC均未标记为最终,因为没有一个包含原始 NFA 的最终状态。

On a ., our NFA may move from State 5 to State 6. There are no other transitions on this input from any of the states in A, and there are no epsilon transitions out of State 6. We therefore create the singleton DFA State C as shown. None of States A, B, or C is marked as final, because none contains a final state of the original NFA.

回到不断增长的 DFA 的状态B,我们注意到在输入d时,原始 NFA 可能从状态 2 移动到状态 3 ,或者从状态 8 移动到状态 9 。从状态 3 ,它又可能通过 epsilon 转换移动到状态 2、4、5 或 8 。由于这些正是B中已有的状态,因此我们在 DFA 中创建一个自循环。另一方面,给定 . ,原始 NFA 可能从状态 5 移动到状态 6 ,或者从状态 9 移动到状态 10 。从状态 10 ,它又可能通过 epsilon 转换移动到状态 11、12 或 14 。因此,我们创建如图所示的 DFA 状态D,其中 . 转换从BD。状态D被标记为最终状态,因为它包含原始 NFA 的状态 14 。也就是说,给定输入d . ,存在一条从原始 NFA 的起始状态到终止状态的路径。继续枚举状态集,我们最终创建了另外三个状态集,在图 2.9中标记为EFG。与状态D一样,它们都包含原始 NFA 的状态 14,因此被标记为最终状态。■

Returning to State B of the growing DFA, we note that on an input of d the original NFA may move from State 2 to State 3, or from State 8 to State 9. From State 3, in turn, it may move to States 2, 4, 5, or 8 via epsilon transitions. As these are exactly the states already in B, we create a self-loop in the DFA. Given a ., on the other hand, the original NFA may move from State 5 to State 6, or from State 9 to State 10. From State 10, in turn, it may move to States 11, 12, or 14 via epsilon transitions. We therefore create DFA State D as shown, with a transition on . from B to D. State D is marked as final because it contains state 14 of the original NFA. That is, given input d., there exists a path from the start state to the end state of the original NFA. Continuing our enumeration of state sets, we end up creating three more, labeled E, F, and G in Figure 2.9. Like State D, these all contain State 14 of the original NFA, and thus are marked as final. ■

在我们的示例中,DFA 最终比 NFA 小,但这只是因为我们的常规语言非常简单。理论上,DFA 中的状态数可能是 NFA 中状态数的指数,但这种极端情况在实践中也不常见。对于编程语言扫描器,DFA 往往比 NFA 大,但并不离谱。我们将在 C-2.4.1 节中更详细地考虑空间复杂性。

In our example, the DFA ends up being smaller than the NFA, but this is only because our regular language is so simple. In theory, the number of states in the DFA may be exponential in the number of states in the NFA, but this extreme is also uncommon in practice. For a programming language scanner, the DFA tends to be larger than the NFA, but not outlandishly so. We consider space complexity in more detail in Section C-2.4.1.

最小化 DFA

Minimizing the DFA

例 2.15

Example 2.15

d *( . d | d . ) d *的最小 DFA

Minimal DFA for d*( .d | d. ) d*

从正则表达式开始,我们现在构建了一个等效的 DFA。虽然这个 DFA 有七个状态,但经过一番思考,我们发现应该存在一个更小的状态。特别是,一旦我们看到 d.,唯一有效的转换就在d上,我们应该能够使用单个最终状态。我们可以通过以下归纳构造将这种直觉形式化,从而将其应用于任何 DFA。

Starting from a regular expression, we have now constructed an equivalent DFA. Though this DFA has seven states, a bit of thought suggests that a smaller one should exist. In particular, once we have seen both a d and a ., the only valid transitions are on d, and we ought to be able to make do with a single final state. We can formalize this intuition, allowing us to apply it to any DFA, via the following inductive construction.

最初,我们将(不一定是最小的)DFA 的状态放入两个等价类:最终状态和非最终状态。然后,我们反复搜索等价类κ和输入符号c,使得当给定c作为输入时, κ中的状态会转换为k > 1 个不同等价类中的状态。然后,我们将κ划分为k个类,使得给定新类中的所有状态都会移动到c上的同一旧类的成员。当我们无法找到以这种方式划分的类时,我们就完成了。

Initially we place the states of the (not necessarily minimal) DFA into two equivalence classes: final states and nonfinal states. We then repeatedly search for an equivalence class κ and an input symbol c such that when given c as input, the states in κ make transitions to states in k > 1 different equivalence classes. We then partition κ into k classes in such a way that all states in a given new class would move to a member of the same old class on c. When we are unable to find a class to partition in this fashion we are done.

在我们的例子中,原来的放置方式是将状态D 、 E 、 FG放在一个类 (最终状态) 中,而将状态ABC放在另一个类中,如图2.10的左上角所示。不幸的是,起始状态在d和 . 上都有歧义转换。为了解决d 的歧义,我们将ABC拆分为ABC,如右上角所示。新状态AB在d上有一个自循环;新状态C移动到状态DEFG。但是,状态AB在 . 上仍然有歧义,我们通过将其拆分为状态AB来解决此问题,如图底部所示。此时没有进一步的歧义,我们剩下四状态最小 DFA。■

In our example, the original placement puts States D, E, F, and G in one class (final states) and States A, B, and C in another, as shown in the upper left of Figure 2.10. Unfortunately, the start state has ambiguous transitions on both d and .. To address the d ambiguity, we split ABC into AB and C, as shown in the upper right. New State AB has a self-loop on d; new State C moves to State DEFG. State AB still has an ambiguity on ., however, which we resolve by splitting it into States A and B, as shown at the bottom of the figure. At this point there are no further ambiguities, and we are left with a four-state minimal DFA. ■

电话02-10-9780124104099
图 2.10 图 2.9的 DFA 的最小化 。在每一步中,我们拆分一组状态以消除转换歧义

2.2.2 扫描器代码

2.2.2 Scanner Code

我们可以通过两种主要方式中的任一种实现一个扫描器,以明确捕获 DFA 的“圆圈和箭头”结构。一种方式是使用goto或嵌套case (switch)语句将自动机嵌入到程序的控制流中;另一种方式(如下一小节所述)使用表和驱动程序。一般来说,手写自动机倾向于使用嵌套 case 语句,而大多数自动生成的自动机使用表。表很难手动创建,但比在程序内部创建代码更容易。同样,嵌套 case 语句比图 2.5中的临时方法更容易编写和调试,尽管效率不如图 2.5 那么高。Unix 的lex/flex工具生成包含表和自定义驱动程序的 C 语言输出。

We can implement a scanner that explicitly captures the “circles-and-arrows” structure of a DFA in either of two main ways. One embeds the automaton in the control flow of the program using gotos or nested case (switch) statements; the other, described in the following subsection, uses a table and a driver. As a general rule, handwritten automata tend to use nested case statements, while most automatically generated automata use tables. Tables are hard to create by hand, but easier than code to create from within a program. Likewise, nested case statements are easier to write and to debug than the ad hoc approach of Figure 2.5, if not quite as efficient. Unix's lex/flex tool produces C language output containing tables and a customized driver.

例 2.16

Example 2.16

嵌套case语句自动化

Nested case statement automation

嵌套 case 语句风格的自动机具有以下一般结构:

The nested case statement style of automaton has the following general structure:

state := 1 –– 起始状态

state := 1 –– start state

环形

loop

 读取当前字符

 read cur char

 案件状态

 case state of

  1:案例 cur_char

  1 : case cur_char of

   ' ', '\t', '\n' : …

   ‘ ’, ‘\t’, ‘\n’ : …

   ‘a’…‘z’:…

   ‘a’… ‘z’ : …

   '0'…'9':...

   ‘0’… ‘9’ : …

   '>' :…

   ‘>’ : …

   

   

  2:案例 cur_char

  2 : case cur_char of

   

   

   

   

  n: 案例 cur_char

  n: case cur_char of

   

   

外部case语句涵盖有限自动机的状态。内部case语句涵盖每个状态的转换。大多数内部子句只是设置一个新状态。有些从扫描器返回当前标记。(如果当前字符不应该是该标记的一部分,它将被推回到输入流然后再返回。)■

The outer case statement covers the states of the finite automaton. The inner case statements cover the transitions out of each state. Most of the inner clauses simply set a new state. Some return from the scanner with the current token. (If the current character should not be part of that token, it is pushed back onto the input stream before returning.) ■

代码的两个方面通常与形式有限自动机的严格形式不同。一个是关键字的处理。另一个是需要提前预知何时标记可以有效地扩展两个或更多个附加字符,但不能只扩展一个。

Two aspects of the code typically deviate from the strict form of a formal finite automaton. One is the handling of keywords. The other is the need to peek ahead when a token can validly be extended by two or more additional characters, but not by only one.

如第 2.1.1 节开头所述,大多数语言中的关键字看起来就像标识符,但保留用于特殊用途(一些作者使用术语“保留字”而不是“关键字”)。可以编写一个区分关键字和标识符的有限自动机,但它需要很多状态(参见练习 2.3)。因此,大多数扫描器(无论是手写还是自动生成的)都将关键字视为标识符规则的“例外”。在返回之前解析器的标识符,扫描器会在哈希表或 trie(分支路径树)中查找它,以确保它不是真正的关键字。10

As noted at the beginning of Section 2.1.1, keywords in most languages look just like identifiers, but are reserved for a special purpose (some authors use the term reserved word instead of keyword). It is possible to write a finite automaton that distinguishes between keywords and identifiers, but it requires a lot of states (see Exercise 2.3). Most scanners, both handwritten and automatically generated, therefore treat keywords as “exceptions” to the rule for identifiers. Before returning an identifier to the parser, the scanner looks it up in a hash table or trie (a tree of branching paths) to make sure it isn't really a keyword.10

设计与实现

Design & Implementation

2.4 识别多种 token

2.4 Recognizing multiple kinds of token

扫描器与正式 DFA 的主要区别之一是,它除了识别标记之外,还识别标记。也就是说,它不仅确定字符是否构成有效标记;它还指示一个。实际上,这意味着它必须为每种标记设置单独的最终状态。我们在 RE-to-DFA 构造中略过了这个问题。

One of the chief ways in which a scanner differs from a formal DFA is that it identifies tokens in addition to recognizing them. That is, it not only determines whether characters constitute a valid token; it also indicates which one. In practice, this means that it must have separate final states for every kind of token. We glossed over this issue in our RE-to-DFA constructions.

要为具有n种不同标记的语言构建扫描器,我们从此处图中建议的 NFA 开始。给定 NFA M i,1 ≤ in(每种标记一个自动机),我们创建一个新的起始状态,其中 epsilon 转换到M i起始状态。但是,与图 2.7(c)中的交替构造相反,我们不会创建单个最终状态;我们保留现有的状态,每个状态都标有其最终标记的标记。然后我们像以前一样应用 NFA-to-DFA 构造。(如果 NFA 中不同标记的最终状态最终处于 DFA 的相同状态,则我们有不明确的标记定义。这些问题可以通过更改派生 NFA 的正则表达式或在 DFA 周围包装其他逻辑来解决。)

To build a scanner for a language with n different kinds of tokens, we begin with an NFA of the sort suggested in the figure here. Given NFAs Mi, 1 ≤ in (one automaton for each kind of token), we create a new start state with epsilon transitions to the start states of the Mis. In contrast to the alternation construction of Figure 2.7(c), however, we do not create a single final state; we keep the existing ones, each labeled by the token for which it is final. We then apply the NFA-to-DFA construction as before. (If final states for different tokens in the NFA ever end up in the same state of the DFA, then we have ambiguous token definitions. These may be resolved by changing the regular expressions from which the NFAs were derived, or by wrapping additional logic around the DFA.)

u02-01-9780124104099

在 DFA 最小化构造中,我们不是从两个等价类(最终状态和非最终状态)开始,而是从n + 1 开始,包括每种标记类型的最终状态的单独类。练习 2.5探索了这种构造,用于识别示例 2.3中的整数小数类型的扫描仪。

In the DFA minimization construction, instead of starting with two equivalence classes (final and nonfinal states), we begin with n + 1, including a separate class for final states for each of the kinds of token. Exercise 2.5 explores this construction for a scanner that recognizes both the integer and decimal types of Example 2.3.

例 2.17

Example 2.17

非平凡前缀问题

The nontrivial prefix problem

每当一个合法标记是另一个标记的前缀时,“最长标记”规则就表明我们应该继续扫描。但是,如果某些中间字符串不是有效标记,我们无法判断是否有可能出现更长的标记,除非我们向前查看多个字符。这个问题出现在 C 中的点字符(句点)上。假设扫描器刚刚看到 3,并且输入中有一个点。它需要查看点以外的字符,以区分3.14(表示实数的单个标记)、3 . foo(扫描器应该接受的三个标记,即使解析器反对按该顺序查看它们)和3 … foo(同样在语法上无效,但仍然是三个独立的标记)。通常,扫描器必须检查以做出决定的即将到来的字符称为其前瞻。在第 2.3 节中,我们将在解析中看到类似的前瞻标记概念。■

Whenever one legitimate token is a prefix of another, the “longest possible token” rule says that we should continue scanning. If some of the intermediate strings are not valid tokens, however, we can't tell whether a longer token is possible without looking more than one character ahead. This problem arises with dot characters (periods) in C. Suppose the scanner has just seen a 3 and has a dot coming up in the input. It needs to peek at characters beyond the dot in order to distinguish between 3.14 (a single token designating a real number), 3 . foo (three tokens that the scanner should accept, even though the parser will object to seeing them in that order), and 3 … foo (again not syntactically valid, but three separate tokens nonetheless). In general, upcoming characters that a scanner must examine in order to make a decision are known as its look-ahead. In Section 2.3 we will see a similar notion of look-ahead tokens in parsing. ■

例 2.18

Example 2.18

Fortran 扫描中的前瞻

Look-ahead in Fortran scanning

在较为复杂的语言中,扫描器可能需要提前任意距离查看。例如,在 Fortran IV 中,DO 5 I = 1,25是循环的标题(对于I的值从 1 到25 ,它执行直到标号为5的语句),而DO 5 I = 1.25是一条赋值语句,它将值1.25放入变量DO5I中。在(Fortran 90 之前的版本)Fortran 输入中会忽略空格,即使在变量名中间也是如此。此外,变量无需声明,DO循环的终止符只是一个标签,解析器可以忽略它。看到DO之后,扫描器无法判断5是否是当前标记的一部分,直到它到达逗号或点。人们普遍(但显然是错误的)声称 NASA 的水手 1 号太空探测器丢失是因为意外将逗号替换为点,飞行控制软件中的情况与此类似。从 Fortran 77 开始的11种 Fortran 方言允许(实际上鼓励)使用替代循环头的语法,其中额外的逗号可以减少误解的可能性:DO 5,I = 1,25。■

In messier languages, a scanner may need to look an arbitrary distance ahead. In Fortran IV, for example, DO 5 I = 1,25 is the header of a loop (it executes the statements up to the one labeled 5 for values of I from 1 to 25), while DO 5 I = 1.25 is an assignment statement that places the value 1.25 into the variable DO5I. Spaces are ignored in (pre-Fortran 90) Fortran input, even in the middle of variable names. Moreover, variables need not be declared, and the terminator for a DO loop is simply a label, which the parser can ignore. After seeing DO, the scanner cannot tell whether the 5 is part of the current token until it reaches the comma or dot. It has been widely (but apparently incorrectly) claimed that NASA's Mariner 1 space probe was lost due to accidental replacement of a comma with a dot in a case similar to this one in flight control software.11 Dialects of Fortran starting with Fortran 77 allow (in fact encourage) the use of alternative syntax for loop headers, in which an extra comma makes misinterpretation less likely: DO 5,I = 1,25. ■

设计与实现

Design & Implementation

2.5 最长可能的标记

2.5 Longest possible tokens

在语法设计中稍加注意(避免使用其他标记的非平凡前缀的标记)可以大大简化扫描。在前缀歧义的简单情况下,扫描器可以自动执行“最长可能的标记”规则。然而,在 Fortran 中,规则非常复杂,以至于没有纯粹的词汇解决方案可以满足需要。Dyadkin 的一篇文章 [ Dya95 ] 讨论了其中一些问题以及可能的解决方案。

A little care in syntax design—avoiding tokens that are nontrivial prefixes of other tokens—can dramatically simplify scanning. In straightforward cases of prefix ambiguity, the scanner can enforce the “longest possible token” rule automatically. In Fortran, however, the rules are sufficiently complex that no purely lexical solution suffices. Some of the problems, and a possible solution, are discussed in an article by Dyadkin [Dya95].

在 C 语言中,点字符问题可以很容易地作为特殊情况处理。在需要大量前瞻的语言中,扫描器可以采用更通用的方法。在任何歧义情况下,它都假设可以使用较长的标记,但会记住较短的标记可能在过去的某个时间点被识别。它还会缓冲较短标记末尾之后读取的所有字符。如果乐观的假设导致扫描器进入错误状态,它会“取消读取”缓冲的字符,以便稍后再次看到它们,并返回较短的标记。

In C, the dot character problem can easily be handled as a special case. In languages requiring larger amounts of look-ahead, the scanner can take a more general approach. In any case of ambiguity, it assumes that a longer token will be possible, but remembers that a shorter token could have been recognized at some point in the past. It also buffers all characters read beyond the end of the shorter token. If the optimistic assumption leads the scanner into an error state, it “unreads” the buffered characters so that they will be seen again later, and returns the shorter token.

2.2.3 表驱动扫描

2.2.3 Table-Driven Scanning

例 2.19

Example 2.19

表格驱动扫描

Table-driven scanning

在前面的小节中,我们概述了如何使用控制流(循环和嵌套的 case 语句)来表示有限自动机。 另一种方法是将自动机表示为一种数据结构:二维转换表。 驱动程序(图 2.11)使用当前状态和输入字符作为该表的索引。 表中的每个条目指定是否移动到新状态(如果是,则移动到哪一个状态)、返回标记还是宣布错误。 第二个表指示对于每个状态我们是否可能位于标记的末尾(如果是,则位于哪一个)。 将第二个表与第一个表分开可以让我们注意到何时经过可能是标记末尾的状态,因此如果遇到错误状态,我们可以后退。图 2.12显示了我们的计算器标记的示例表。

In the preceding subsection we sketched how control flow—a loop and nested case statements—can be used to represent a finite automaton. An alternative approach represents the automaton as a data structure: a two-dimensional transition table. A driver program (Figure 2.11) uses the current state and input character to index into the table. Each entry in the table specifies whether to move to a new state (and if so, which one), return a token, or announce an error. A second table indicates, for each state, whether we might be at the end of a token (and if so, which one). Separating this second table from the first allows us to notice when we pass a state that might have been the end of a token, so we can back up if we hit an error state. Example tables for our calculator tokens appear in Figure 2.12.

传真:02-11-9780124104099
图 2.11 表驱动扫描仪的驱动程序,其代码用于处理歧义情况,其中一个有效标记是另一个有效标记的前缀,但某些中间字符串不是。
电话02-12-9780124104099
图 2.12 计算器语言的扫描表。图 2.11的代码可以使用这些表。状态的编号与图 2.6相同,但增加了两个状态(17 和 18)来“识别”空格和注释。右侧列表示表 token_tab;该图的其余部分表示 scan_tab。表中的数字表示对应操作为 move 的条目。破折号表示无法扩展当前标记:如果 token_tab 中的对应条目非空,则操作为识别;否则,操作为错误。表 keyword_tab(未显示)包含字符串readwrite

就像手写扫描仪一样,图 2.11中的表格驱动代码在返回之前立即在关键字表中查找标记。外循环用于过滤掉注释和“空白”——空格、制表符和换行符。■

Like a handwritten scanner, the table-driven code of Figure 2.11 looks tokens up in a table of keywords immediately before returning. An outer loop serves to filter out comments and “white space”—spaces, tabs, and newlines. ■

2.2.4 词汇错误

2.2.4 Lexical Errors

图 2.11中的代码明确地认识到了词汇错误的可能性。在某些情况下,输入的下一个字符可能既不是当前标记的可接受延续,也不是另一个标记的开始。在这种情况下,扫描器必须打印一条错误消息并执行某种恢复,以便编译可以继续,即使只是为了查找其他错误。幸运的是,词汇错误相对罕见——大多数字符序列确实对应于标记序列——并且相对容易处理。最常见的方法是简单地 (1) 丢弃当前无效的标记;(2) 向前跳,直到找到可以合法开始新标记的字符;(3) 重新启动扫描算法;(4) 依靠解析器的错误恢复机制来应对任何结果标记序列在语法上无效的情况。当然,错误恢复的需求并不是表驱动扫描器所独有的;任何扫描器都必须应对错误。我们没有在图 2.5中展示代码,但在实践中它必须存在。

The code in Figure 2.11 explicitly recognizes the possibility of lexical errors. In some cases the next character of input maybe neither an acceptable continuation of the current token nor the start of another token. In such cases the scanner must print an error message and perform some sort of recovery so that compilation can continue, if only to look for additional errors. Fortunately, lexical errors are relatively rare—most character sequences do correspond to token sequences—and relatively easy to handle. The most common approach is simply to (1) throw away the current, invalid token; (2) skip forward until a character is found that can legitimately begin a new token; (3) restart the scanning algorithm; and (4) count on the error-recovery mechanism of the parser to cope with any cases in which the resulting sequence of tokens is not syntactically valid. Of course the need for error recovery is not unique to table-driven scanners; any scanner must cope with errors. We did not show the code in Figure 2.5, but it would have to be there in practice.

图 2.11中的代码还表明,扫描器必须返回找到的标记类型及其字符串映像(拼写);同样,此要求适用于所有类型的扫描器。对于某些标记,字符串映像是多余的:毕竟,所有分号看起来都一样,所有while关键字也是如此。但是,对于其他标记(例如,标识符、字符串和数字常量),映像是语义分析所必需的。它对于错误消息也很有用:“未声明的标识符”不如“ foo未声明”好。

The code in Figure 2.11 also shows that the scanner must return both the kind of token found and its character-string image (spelling); again this requirement applies to all types of scanners. For some tokens the character-string image is redundant: all semicolons look the same, after all, as do all while keywords. For other tokens, however (e.g., identifiers, character strings, and numeric constants), the image is needed for semantic analysis. It is also useful for error messages: “undeclared identifier” is not as nice as “foo has not been declared.”

2.2.5 指令

2.2.5 Pragmas

某些语言和语言实现允许程序包含称为pragma 的构造,这些构造为编译器提供指令或提示。不会改变程序语义(仅改变编译过程)的 pragma 有时被称为重要注释。在某些语言中,这个名称也很合适,因为像注释一样,pragma 可以出现在源程序中的任何位置。在这种情况下,它们通常由扫描器处理:允许它们出现在语法中的任何位置会使解析器变得非常复杂。在大多数语言中,然而,指令只允许出现在语法中某些明确定义的位置。在这种情况下,它们最好由解析器或语义分析器来处理。

Some languages and language implementations allow a program to contain constructs called pragmas that provide directives or hints to the compiler. Pragmas that do not change program semantics—only the compilation process—are sometimes called significant comments. In some languages the name is also appropriate because, like comments, pragmas can appear anywhere in the source program. In this case they are usually processed by the scanner: allowing them anywhere in the grammar would greatly complicate the parser. In most languages, however, pragmas are permitted only at certain well-defined places in the grammar. In this case they are best processed by the parser or semantic analyzer.

作为指令的语句可能

Pragmas that serve as directives may

 打开或关闭各种运行时检查(例如指针或下标检查)

 Turn various kinds of run-time checks (e.g., pointer or subscript checking) on or off

 打开或关闭某些代码改进(例如,在内部循环中打开以提高性能;关闭以提高编译速度)

 Turn certain code improvements on or off (e.g., on in inner loops to improve performance; off otherwise to improve compilation speed)

 启用或禁用性能分析(收集统计信息以识别程序瓶颈)

 Enable or disable performance profiling (statistics gathering to identify program bottlenecks)

一些指令“越界”并改变程序语义。例如,在 Ada 中,unchecked指令可用于禁用类型检查。在 OpenMP(我们将在第 13 章中讨论)中,指令指定了 Fortran、C 和 C++ 的重要并行扩展:创建、调度和同步线程。在这种情况下,将扩展表示为指令而不是更深入的集成更改的主要原因是明确划分核心语言和扩展之间的界限,并跨语言共享一组通用的扩展。

Some directives “cross the line” and change program semantics. In Ada, for example, the unchecked pragma can be used to disable type checking. In OpenMP, which we will consider in Chapter 13, pragmas specify significant parallel extensions to Fortran, C and C++: creating, scheduling, and synchronizing threads. In this case the principal rationale for expressing the extensions as pragmas rather than more deeply integrated changes is to sharply delineate the boundary between the core language and the extensions, and to share a common set of extensions across languages.

用作提示的指令为编译器提供了有关源程序的信息,可能使它能够更好地完成工作:

Pragmas that serve (merely) as hints provide the compiler with information about the source program that may allow it to do a better job:

 变量x使用非常频繁(将其保存在寄存器中可能是一个好主意)。

 Variable x is very heavily used (it may be a good idea to keep it in a register).

 子程序F是一个纯函数:它对程序其余部分的唯一影响是它返回的值。

 Subroutine F is a pure function: its only effect on the rest of the program is the value it returns.

 子程序S不是(间接)递归的(其存储可能是静态分配的)。

 Subroutine S is not (indirectly) recursive (its storage may be statically allocated).

 对于浮点变量x来说,32 位精度(而不是 64 位)就足够了。

 32 bits of precision (instead of 64) suffice for floating-point variable x.

编译器可能会为了简单起见或者面对矛盾的信息而忽略这些。

The compiler may ignore these in the interest of simplicity, or in the face of contradictory information.

C++11 中引入了编译指示的标准语法(它们被称为“属性”)。例如,可以将一个打印错误消息并终止执行的函数标记为 [[ noreturn ]],以允许编译器优化围绕调用的代码,或者发出更有帮助的错误或警告消息。截至撰写本文时,供应商可以扩展支持的属性集(通过修改编译器),但普通程序员不能扩展。这些属性应限于提示(而不是指令)的程度一直存在争议。Java(称为“批注”)和 C#(称为“属性”)中的新编译指示可由程序员定义;我们将在第 16.3.1 节中返回讨论这些内容。

Standard syntax for pragmas was introduced in C++11 (where they are known as “attributes”). A function that prints an error message and terminates execution, for example, can be labeled [[noreturn]], to allow the compiler to optimize code around calls, or to issue more helpful error or warning messages. As of this writing, the set of supported attributes can be extended by vendors (by modifying the compiler), but not by ordinary programmers. The extent to which these attributes should be limited to hints (rather than directives) has been somewhat controversial. New pragmas in Java (which calls them “annotations”) and C# (which calls them “attributes”) can be defined by the programmer; we will return to these in Section 16.3.1.

02-01-9780124104099检查你的理解

Check Your Understanding

10. 列出典型扫描仪执行的任务。

10. List the tasks performed by the typical scanner.

11. 与手写扫描器相比,自动生成的扫描器有哪些优势?为什么许多商业编译器都使用手写扫描器?

11. What are the advantages of an automatically generated scanner, in comparison to a handwritten one? Why do many commercial compilers use a handwritten scanner anyway?

12. 解释确定性和非确定性有限自动机之间的区别。为什么我们更喜欢确定性自动机进行扫描?

12. Explain the difference between deterministic and nondeterministic finite automata. Why do we prefer the deterministic variety for scanning?

13. 概述将一组正则表达式转换为最小 DFA 所使用的构造。

13. Outline the constructions used to turn a set of regular expressions into a minimal DFA.

14.  “最长可能的令牌”规则是什么?

14. What is the “longest possible token” rule?

15. 为什么扫描仪有时必须“偷看”即将出现的字符?

15. Why must a scanner sometimes “peek” at upcoming characters?

16. 关键字和标识符有什么区别

16. What is the difference between a keyword and an identifier?

17. 为什么扫描器必须保存token的文本?

17. Why must a scanner save the text of tokens?

18. 扫描仪如何识别词汇错误?它如何响应?

18. How does a scanner identify lexical errors? How does it respond?

19. 什么是语用?

19. What is a pragma?

2.3 解析

2.3 Parsing

解析器是典型编译器的核心。它调用扫描器来获取输入程序的标记,将标记组合成语法树,并将该树(可能一次一个子例程)传递给编译器的后续阶段,执行语义分析和代码生成和改进。实际上,解析器“负责”整个编译过程;这种编译风格有时被称为语法制导翻译

The parser is the heart of a typical compiler. It calls the scanner to obtain the tokens of the input program, assembles the tokens together into a syntax tree, and passes the tree (perhaps one subroutine at a time) to the later phases of the compiler, which perform semantic analysis and code generation and improvement. In effect, the parser is “in charge” of the entire compilation process; this style of compilation is sometimes referred to as syntax-directed translation.

如本章简介中所述,上下文无关文法 (CFG) 是CF 语言的生成器。解析器是一种语言识别器。可以证明,对于任何 CFG,我们都可以创建一个运行时间为O ( n3 ) 的解析器,其中n是输入程序的长度。12两种著名的解析算法可以达到这个界限:Earley 算法 [ Ear70 ] 和 Cocke-Younger-Kasami (CYK) 算法 [ Kas65You67 ]。立方时间对于解析大型程序来说太慢了,但幸运的是,并非所有语法都需要这种通用且缓慢的解析算法。我们可以为很多类语法构建以线性时间运行的解析器。其中最重要的两个类称为 LL 和 LR(图 2.13)。

As noted in the introduction to this chapter, a context-free grammar (CFG) is a generator for a CF language. A parser is a language recognizer. It can be shown that for any CFG we can create a parser that runs in O(n3) time, where n is the length of the input program.12 There are two well-known parsing algorithms that achieve this bound: Earley's algorithm [Ear70] and the Cocke-Younger-Kasami (CYK) algorithm [Kas65, You67]. Cubic time is much too slow for parsing sizable programs, but fortunately not all grammars require such a general and slow parsing algorithm. There are large classes of grammars for which we can build parsers that run in linear time. The two most important of these classes are called LL and LR (Figure 2.13).

电话02-13-9780124104099
图 2.13 线性时间解析算法的主要类别。

LL 代表“从左到右,最左派生”。LR 代表“从左到右,最右派生”。在这两个类中,输入都是从左到右读取的,并且解析器尝试发现(构造)该输入的派生。对于 LL 解析器,派生将是最左边的;对于 LR 解析器,派生将是最右边的。我们将首先介绍 LL 解析器。它们通常被认为更简单且更容易理解。它们可以手写,也可以由解析器生成工具从适当的语法自动生成。LR 语法类更大(即,LR 语法比 LL 语法更多),有些人发现 LR 语法的结构更直观,尤其是在处理算术表达式时。LR 解析器几乎总是由解析器生成工具构建。这两类解析器都用于生产编译器,尽管 LR 解析器更常见。

LL stands for “Left-to-right, Left-most derivation.” LR stands for “Left-to-right, Right-most derivation.” In both classes the input is read left-to-right, and the parser attempts to discover (construct) a derivation of that input. For LL parsers, the derivation will be left-most; for LR parsers, right-most. We will cover LL parsers first. They are generally considered to be simpler and easier to understand. They can be written by hand or generated automatically from an appropriate grammar by a parser-generating tool. The class of LR grammars is larger (i.e., more grammars are LR than LL), and some people find the structure of the LR grammars more intuitive, especially in the handling of arithmetic expressions. LR parsers are almost always constructed by a parser-generating tool. Both classes of parsers are used in production compilers, though LR parsers are more common.

LL 解析器也称为“自上而下”或“预测”解析器。它们从根部向下构建解析树,在每个步骤中根据下一个可用的输入标记预测将使用哪个生成式来扩展当前节点。LR 解析器也称为“自下而上”解析器。它们从叶子向上构建解析树,识别何时可以将一组叶子或其他节点连接在一起作为单个父节点的子节点。

LL parsers are also called “top-down,” or “predictive” parsers. They construct a parse tree from the root down, predicting at each step which production will be used to expand the current node, based on the next available token of input. LR parsers are also called “bottom-up” parsers. They construct a parse tree from the leaves up, recognizing when a collection of leaves or other nodes can be joined together as the children of a single parent.

例 2.20

Example 2.20

自上而下和自下而上的解析

Top-down and bottom-up parsing

我们可以通过一个简单的例子来说明自上而下和自下而上解析之间的区别。考虑以下以逗号分隔的标识符列表的语法,以分号结尾:

We can illustrate the difference between top-down and bottom-up parsing by means of a simple example. Consider the following grammar for a comma-separated list of identifiers, terminated by a semicolon:

id_list → id id_list_tail

id_list → id id_list_tail

id_list_tail →,id id_list_tail

id_list_tail →, id id_list_tail

id_列表_尾部→;

id_list_tail → ;

这些是通常用于自上而下解析器中的标识符列表的产生式。它们也可以自下而上地解析(大多数自上而下的语法都可以)。实际上,它们不会在自下而上的解析器中使用,原因稍后会清楚,但能够以任何一种方式处理它们使它们非常适合此示例。

These are the productions that would normally be used for an identifier list in a top-down parser. They can also be parsed bottom-up (most top-down grammars can be). In practice they would not be used in a bottom-up parser, for reasons that will become clear in a moment, but the ability to handle them either way makes them good for this example.

图 2.14显示了自上而下和自下而上构建字符串ABC;的解析树的渐进阶段。自上而下的解析器首先预测树的根(id_list)将扩展为id id_list_tail。然后将 id 与从扫描器获取的标记进行匹配。(如果扫描器生成了不同的结果,解析器将宣布语法错误。)然后,解析器向下移动到第一个(在本例中仅此一个)非终结子节点,并预测 id_list_tail扩展为id id_list_tail。为了做出这个预测,它需要查看即将到来的标记(逗号),这使它能够在id_list_tail的两种可能扩展之间进行选择。然后,它匹配逗号和id,并向下移动到下一个id_list_tail。以类似的递归方式,自上而下的解析器沿着树从左到右进行工作,预测和扩展节点并追踪树边缘最左边的派生。

Progressive stages in the top-down and bottom-up construction of a parse tree for the string A, B, C; appear in Figure 2.14. The top-down parser begins by predicting that the root of the tree (id_list) will expand to id id_list_tail. It then matches the id against a token obtained from the scanner. (If the scanner produced something different, the parser would announce a syntax error.) The parser then moves down into the first (in this case only) nonterminal child and predicts that id_list_tail will expand to, id id_list_tail. To make this prediction it needs to peek at the upcoming token (a comma), which allows it to choose between the two possible expansions for id_list_tail. It then matches the comma and the id and moves down into the next id_list_tail. In a similar, recursive fashion, the top-down parser works down the tree, left-to-right, predicting and expanding nodes and tracing out a left-most derivation of the fringe of the tree.

传真:02-14-9780124104099
图 2.14 对输入字符串 A、B、C进行自上而下()和自下而上解析(;语法出现在左下方。

相比之下,自下而上的解析器首先注意到树的最左边的叶子是一个id。下一个叶子是一个逗号,再下一个叶子是另一个id。解析器以这种方式继续,将新的叶子从扫描器移到部分完成的解析树片段的森林中,直到它意识到其中一些片段构成了一个完整的右侧。在这个语法中,直到解析器看到分号(id_list_tail → ; 的右侧)时才会发生这种情况。有了这个右侧,解析器将分号简化为id_list_tail。然后它将id id_list_tail简化为另一个id_list_tail。再做一次之后,它就能够将id id_list_tail简化为解析树的根id_list

The bottom-up parser, by contrast, begins by noting that the left-most leaf of the tree is an id. The next leaf is a comma and the one after that is another id. The parser continues in this fashion, shifting new leaves from the scanner into a forest of partially completed parse tree fragments, until it realizes that some of those fragments constitute a complete right-hand side. In this grammar, that doesn't occur until the parser has seen the semicolon—the right-hand side of id_list_tail → ;. With this right-hand side in hand, the parser reduces the semicolon to an id_list_tail. It then reduces, id id_list_tail into another id_list_tail. After doing this one more time it is able to reduce id id_list_tail into the root of the parse tree, id_list.

自下而上的解析器从来不会预测接下来会看到什么。相反,它会将标记移入其森林,直到识别出右侧,然后将其归约到左侧。由于这种行为,自下而上的解析器有时被称为移位归约解析器。从下往上移动图片,我们可以看到移位归约解析器反向追踪最右侧的推导。由于自下而上的解析器是第一个得到仔细正式研究的解析器,因此最右侧的推导有时被称为规范的。■

At no point does the bottom-up parser predict what it will see next. Rather, it shifts tokens into its forest until it recognizes a right-hand side, which it then reduces to a left-hand side. Because of this behavior, bottom-up parsers are sometimes called shift-reduce parsers. Moving up the figure, from bottom to top, we can see that the shift-reduce parser traces out a right-most derivation, in reverse. Because bottom-up parsers were the first to receive careful formal study, right-most derivations are sometimes called canonical. ■

LR 解析器有几个重要的子类,包括 SLR、LALR 和“完整 LR”。SLR 和 LALR 因其易于实现而重要,而完整 LR 因其通用性而重要。LL 解析器也可以分为 SLL 和“完整 LL”子类。我们在此仅简要介绍它们之间的差异;有关更多信息,请参阅任何标准编译器构造或解析理论教科书 [ App97ALSU07AU72CT04FCL10GBJ + 12 ]。

There are several important subclasses of LR parsers, including SLR, LALR, and “full LR.” SLR and LALR are important for their ease of implementation, full LR for its generality. LL parsers can also be grouped into SLL and “full LL” subclasses. We will cover the differences among them only briefly here; for further information see any of the standard compiler-construction or parsing theory textbooks [App97, ALSU07, AU72, CT04, FCL10, GBJ+12].

我们通常看到 LL 或 LR(或其他名称)后面带有括号中的数字:例如 LL(2) 或 LALR(1)。此数字表示解析需要多少个前瞻标记。大多数实际编译器只使用一个前瞻标记,但有时使用更多标记会有所帮助。特别是,开源工具 ANTLR 使用多标记前瞻来扩大适合自上而下解析的语言类 [ PQ95 ]。在第 2.3.1 节中,我们将更详细地介绍 LL(1) 语法和手写解析器。在第 2.3.3 节2.3.4节中,我们将考虑自动生成的 LL(1) 和 LR(1)(实际上是 SLR(1))解析器。

One commonly sees LL or LR (or whatever) written with a number in parentheses after it: LL(2) or LALR(1), for example. This number indicates how many tokens of look-ahead are required in order to parse. Most real compilers use just one token of look-ahead, though more can sometimes be helpful. The open-source ANTLR tool, in particular, uses multitoken look-ahead to enlarge the class of languages amenable to top-down parsing [PQ95]. In Section 2.3.1 we will look at LL(1) grammars and handwritten parsers in more detail. In Sections 2.3.3 and 2.3.4 we will consider automatically generated LL(1) and LR(1) (actually SLR(1)) parsers.

例 2.21

Example 2.21

使用自下而上的语法来限制空间

Bounding space with a bottom-up grammar

对于自下而上的解析来说,我们的示例语法存在一个问题,即它迫使编译器将id_list的所有标记移入其森林,然后才能减少其中任何一个。在一个非常大的程序中,我们可能会用尽空间。有时没有什么可以避免大量移位。然而,在这种情况下,我们可以使用另一种语法,允许解析器在解析过程中将id_list的前缀减少为非终结符:

The problem with our example grammar, for the purposes ofbottom-up parsing, is that it forces the compiler to shift all the tokens of an id_list into its forest before it can reduce any of them. In a very large program we might run out of space. Sometimes there is nothing that can be done to avoid a lot of shifting. In this case, however, we can use an alternative grammar that allows the parser to reduce prefixes of the id_list into nonterminals as it goes along:

id_列表id_列表前缀

id_listid_list_prefix ;

id_列表_前缀id_列表_前缀, id

id_list_prefixid_list_prefix, id

 → id

 → id

此语法无法自上而下解析,因为当我们在输入中看到id并且我们期望id_list_prefix时,我们无法分辨应该预测两种可能产生式中的哪一种(有关此困境的更多信息,请参阅第 2.3.2 节)。但是,如图2.15所示,此语法自下而上运行良好。■

This grammar cannot be parsed top-down, because when we see an id on the input and we're expecting an id_list_prefix, we have no way to tell which of the two possible productions we should predict (more on this dilemma in Section 2.3.2). As shown in Figure 2.15, however, the grammar works well bottom-up. ■

电话02-15-9780124104099
图 2.15 自下而上解析 A、B、C;使用允许列表逐步折叠的语法(左下)。

2.3.1 递归下降

2.3.1 Recursive Descent

例 2.22

Example 2.22

自上而下的计算器语言语法

Top-down grammar for a calculator language

为了说明自上而下(预测)解析,我们考虑一下简单“计算器”语言的文法,如图2.16所示。计算器允许将值读入命名变量,然后可在表达式中使用。表达式反过来可以写入输出。控制流是严格线性的(没有循环、if语句或其他跳转)。在我们的许多示例中都会重复出现的一种模式是,我们包括一个初始扩充产生式,programstmt_list $$,它安排程序的“实际”主体(stmt_list)后面跟着一个特殊的结束标记$$ 结束标记由扫描器在输入末尾生成。它的存在允许解析器在看到整个程序后干净地终止,并拒绝接受末尾带有额外垃圾标记的程序。与正则表达式一样,我们使用符号ε表示空字符串。右侧带有ε的产生式有时称为epsilon 产生式

To illustrate top-down (predictive) parsing, let us consider the grammar for a simple “calculator” language, shown in Figure 2.16. The calculator allows values to be read into named variables, which may then be used in expressions. Expressions in turn maybe written to the output. Control flow is strictly linear (no loops, if statements, or other jumps). In a pattern that will repeat in many of our examples, we have included an initial augmenting production, programstmt_list $$, which arranges for the “real” body of the program (stmt_list) to be followed by a special end marker token, $$. The end marker is produced by the scanner at the end of the input. Its presence allows the parser to terminate cleanly once it has seen the entire program, and to decline to accept programs with extra garbage tokens at the end. As in regular expressions, we use the symbol ε to denote the empty string. A production with ε on the right-hand side is sometimes called an epsilon production.

传真:02-16-9780124104099
图 2.16 简单计算器语言的 LL(1) 语法。

将图 2.16expr部分与示例 2.8的表达式语法进行比较可能会有所帮助。大多数人发现以前的 LR 语法更加直观。然而,它存在与示例 2.21的id_list语法类似的问题:如果我们在期望expr时在输入中看到id,我们就无法分辨应该预测两个可能的产生式中的哪一个。图 2.16的语法通过将右侧的公共前缀合并为一个产生式,并使用新符号(term_tailfactor_tail)根据需要生成其他运算符和操作数来避免这个问题。这种转换有一个不良的副作用,即将给定运算符的操作数放在单独的右侧。实际上,我们牺牲了语法的优雅性,以便能够进行预测性解析。■

It may be helpful to compare the expr portion of Figure 2.16 to the expression grammar of Example 2.8. Most people find that previous, LR grammar to be significantly more intuitive. It suffers, however, from a problem similar to that of the id_list grammar of Example 2.21: if we see an id on the input when expecting an expr, we have no way to tell which of the two possible productions to predict. The grammar of Figure 2.16 avoids this problem by merging the common prefixes of right-hand sides into a single production, and by using new symbols (term_tail and factor_tail) to generate additional operators and operands as required. The transformation has the unfortunate side effect of placing the operands of a given operator in separate right-hand sides. In effect, we have sacrificed grammatical elegance in order to be able to parse predictively. ■

那么,我们如何使用计算器语法来解析字符串呢?我们在图 2.14中看到了基本思想。我们从树的顶部开始,根据树中当前最左边的非终结符和当前输入标记预测所需的产生式。我们可以通过两种方式之一来形式化此过程。第一种方法(本小节的其余部分将介绍)是构建一个递归下降解析器,其子例程与语法的非终结符一一对应。递归下降解析器通常是手工构建的,尽管 ANTLR 解析器生成器会根据输入语法自动构建它们。第二种方法(第2.3.3 节中介绍)是构建一个LL 解析表,然后由驱动程序读取。表驱动解析器几乎总是由解析器生成器自动构建。这两个选项(递归下降和表驱动)让人想起嵌套的case语句和表驱动方法构建我们在2.2.22.2.3节中看到的扫描器。需要强调的是,它们实现了相同的基本解析算法。

So how do we parse a string with our calculator grammar? We saw the basic idea in Figure 2.14. We start at the top of the tree and predict needed productions on the basis of the current left-most nonterminal in the tree and the current input token. We can formalize this process in one of two ways. The first, described in the remainder of this subsection, is to build a recursive descent parser whose subroutines correspond, one-one, to the nonterminals of the grammar. Recursive descent parsers are typically constructed by hand, though the ANTLR parser generator constructs them automatically from an input grammar. The second approach, described in Section 2.3.3, is to build an LL parse table which is then read by a driver program. Table-driven parsers are almost always constructed automatically by a parser generator. These two options—recursive descent and table-driven—are reminiscent of the nested case statements and table-driven approaches to building a scanner that we saw in Sections 2.2.2 and 2.2.3. It should be emphasized that they implement the same basic parsing algorithm.

当要解析的语言相对简单,或者没有可用的解析器生成器工具时,最常使用手写递归下降解析器。但是也有例外。特别是,递归下降出现在 GNU 编译器集合 ( gcc ) 的最新版本中。早期版本使用bison自动创建自下而上的解析器。进行此更改部分是出于性能原因,部分是为了能够生成更高质量的语法错误消息。(bison代码更容易编写,而且可以说更易于维护。)

Handwritten recursive descent parsers are most often used when the language to be parsed is relatively simple, or when a parser-generator tool is not available. There are exceptions, however. In particular, recursive descent appears in recent versions of the GNU compiler collection (gcc). Earlier versions used bison to create a bottom-up parser automatically. The change was made in part for performance reasons and in part to enable the generation of higher-quality syntax error messages. (The bison code was easier to write, and arguably easier to maintain.)

例 2.23

Example 2.23

计算器语言的递归下降解析器

Recursive descent parser for the calculator language

图 2.17给出了我们的计算器语言的递归下降解析器的伪代码。它对语法中的每个非终结符都有一个子例程。它还有一个机制input_token来检查扫描器提供的下一个可用标记,以及一个子例程 ( match ) 来使用和更新此标记,并在此过程中验证它是否是预期的标记(由参数指定)。如果match或任何其他子例程看到意外标记,则发生语法错误。暂时让我们假设parse_error子例程只是打印一条消息并终止解析。在第 2.3.5 节中,我们将考虑如何从此类错误中恢复并继续解析输入的其余部分。■

Pseudocode for a recursive descent parser for our calculator language appears in Figure 2.17. It has a subroutine for every nonterminal in the grammar. It also has a mechanism input_token to inspect the next token available from the scanner and a subroutine (match) to consume and update this token, and in the process verify that it is the one that was expected (as specified by an argument). If match or any of the other subroutines sees an unexpected token, then a syntax error has occurred. For the time being let us assume that the parse_error subroutine simply prints a message and terminates the parse. In Section 2.3.5 we will consider how to recover from such errors and continue to parse the remainder of the input. ■

传真:02-17-9780124104099电话02-32-9780124104099
图 2.17 计算器语言的递归下降解析器。执行从过程程序开始。递归调用追踪解析树的遍历。未显示用于保存此树(或某些类似结构)以供编译器的后续阶段使用的代码。

例 2.24

Example 2.24

“求和与平均”程序的递归下降解析

Recursive descent parse of a “sum and average” program

现在假设我们要解析一个简单的程序来读取两个数字并打印它们的总和和平均值:

Suppose now that we are to parse a simple program to read two numbers and print their sum and average:

读A

read A

读B

read B

总和 := A + B

sum := A + B

写总和

write sum

写出总和 / 2

write sum / 2

该程序的解析树如图2.18所示。解析器首先调用子程序program。在注意到初始标记是read之后,program调用stmt_list,然后尝试匹配文件结束伪标记。(在解析树中,根program有两个子节点,stmt_list$$。)过程stmt_list再次注意到即将到来的标记是read。这一观察使其能够确定当前节点(stmt_list)生成stmt stmt_list(而不是ε)。因此,它在返回之前调用stmtstmt_list。以此方式继续,解析器的执行路径描绘出从左到右的深度优先遍历解析树。动态执行跟踪和解析树结构之间的这种对应关系是递归下降解析的显著特征。注意,因为stmt_list非终结符出现在stmt_list产生式的右侧,所以stmt_list子例程必须调用自身。此递归解释了解析技术的名称。■

The parse tree for this program appears in Figure 2.18. The parser begins by calling the subroutine program. After noting that the initial token is a read, program calls stmt_list and then attempts to match the end-of-file pseudotoken. (In the parse tree, the root, program, has two children, stmt_list and $$.) Procedure stmt_list again notes that the upcoming token is a read. This observation allows it to determine that the current node (stmt_list) generates stmt stmt_list (rather than ε). It therefore calls stmt and stmt_list before returning. Continuing in this fashion, the execution path of the parser traces out a left-to-right depth-first traversal of the parse tree. This correspondence between the dynamic execution trace and the structure of the parse tree is the distinguishing characteristic of recursive descent parsing. Note that because the stmt_list nonterminal appears in the right-hand side of a stmt_list production, the stmt_list subroutine must call itself. This recursion accounts for the name of the parsing technique. ■

电话02-18-9780124104099
图 2.18 使用图 2.16的文法,分析示例 2.24的求和平均程序的解析树。

如果没有附加代码(图 2.17中未显示),解析器仅验证程序在语法上是否正确(即,case语句中的parse_error子句均未执行,并且match始终看到它期望看到的内容)。为了对编译器的其余部分有用(必须用其他语言生成等效的目标程序),解析器必须将解析树或程序片段的其他表示保存为显式数据结构。要保存解析树本身,我们可以在执行表示这些子节点的递归子例程和匹配调用之前立即分配并链接记录以表示节点的子节点。我们需要向每个递归例程传递一个参数,该参数指向要扩展的记录(即要发现其子节点)。过程匹配还需要在树的叶子中保存有关某些标记(例如,标识符和文字的字符串表示)的信息。

Without additional code (not shown in Figure 2.17), the parser merely verifies that the program is syntactically correct (i.e., that none of the otherwise parse_error clauses in the case statements are executed and that match always sees what it expects to see). To be of use to the rest of the compiler—which must produce an equivalent target program in some other language—the parser must save the parse tree or some other representation of program fragments as an explicit data structure. To save the parse tree itself, we can allocate and link together records to represent the children of a node immediately before executing the recursive subroutines and match invocations that represent those children. We shall need to pass each recursive routine an argument that points to the record that is to be expanded (i.e., whose children are to be discovered). Procedure match will also need to save information about certain tokens (e.g., character-string representations of identifiers and literals) in the leaves of the tree.

正如我们在第 1 章中看到的,解析树包含大量无关细节,这些细节无需为编译器的其余部分保存。因此,解析器很少显式地构造完整的解析树。它更经常生成抽象语法树或其他更简洁的表示。在递归下降编译器中,可以通过仅在递归调用的子集中分配和链接记录来创建语法树。

As we saw in Chapter 1, the parse tree contains a great deal of irrelevant detail that need not be saved for the rest of the compiler. It is therefore rare for a parser to construct a full parse tree explicitly. More often it produces an abstract syntax tree or some other more terse representation. In a recursive descent compiler, a syntax tree can be created by allocating and linking together records in only a subset of the recursive calls.

编写递归下降解析器最棘手的部分是找出应该用哪些标记来标记case语句的分支。每个分支代表一个产生式:即子程序所对应符号的一种可能的展开。标记X 可以预测产生式,原因二:(1)产生式的右侧在递归展开时,可能产生一个以X开头的字符串,或 (2) 右侧可能不产生任何结果(即,它是ε ,或者是可以递归产生ε的非终结符字符串),并且X可能开始产生接下来的结果。我们将在第 2.3.3 节中使用称为 FIRST 和 FOLLOW 的集合来形式化这个预测概念,并展示如何从 LL(1) CFG 自动推导它们。

The trickiest part of writing a recursive descent parser is figuring out which tokens should label the arms of the case statements. Each arm represents one production: one possible expansion of the symbol for which the subroutine was named. The tokens that label a given arm are those that predict the production. A token X may predict a production for either of two reasons: (1) the right-hand side of the production, when recursively expanded, may yield a string beginning with X, or (2) the right-hand side may yield nothing (i.e., it is ε, or a string of nonterminals that may recursively yield ε), and X may begin the yield of what comes next. We will formalize this notion of prediction in Section 2.3.3, using sets called FIRST and FOLLOW, and show how to derive them automatically from an LL(1) CFG.

02-01-9780124104099检查你的理解

Check Your Understanding

20. 解析的固有“大 O”复杂度是多少?实际编译器中使用的解析器的复杂度是多少?

20. What is the inherent “big-O” complexity of parsing? What is the complexity of parsers used in real compilers?

21. 总结一下 LL 和 LR 解析的区别。哪一个又被称为“自下而上”?“自上而下”?哪一个又被称为“预测”?“移位归约”?“LL”和“LR”分别代表什么?

21. Summarize the difference between LL and LR parsing. Which one of them is also called “bottom-up”? “Top-down”? Which one is also called “predictive”? “Shift-reduce”? What do “LL” and “LR” stand for?

22. 在生产编译器中哪种解析器(自上而下还是自下而上)最常见?

22. What kind of parser (top-down or bottom-up) is most common in production compilers?

23. 为什么最右边的推导有时被称为规范的?

23. Why are right-most derivations sometimes called canonical?

24.LR  (1)中的“1”有什么意义?

24. What is the significance of the “1” in LR(1)?

25. 为什么我们希望(或需要)针对不同的解析算法使用不同的语法?

25. Why might we want (or need) different grammars for different parsing algorithms?

26. 什么是epsilon 产生式

26. What is an epsilon production?

27. 什么是递归下降解析器?为什么它们主要用于小型语言?

27. What are recursive descent parsers? Why are they used mostly for small languages?

28. 解析器如何构造显式的解析树或语法树?

28. How might a parser construct an explicit parse tree or syntax tree?

2.3.2 编写 LL(I) 语法

2.3.2 Writing an LL(I) Grammar

在设计递归下降解析器时,必须掌握编写和修改 LL(1) 语法的一定能力。“LL(1) 性”最常见的两个障碍是左递归公共前缀

When designing a recursive-descent parser, one has to acquire a certain facility in writing and modifying LL(1) grammars. The two most common obstacles to “LL(1)-ness” are left recursion and common prefixes.

例 2.25

Example 2.25

左递归

Left recursion

如果存在非终结符A使得对于某个αA+ A α ,则称该文法为左递归文法。13当产生式右侧的第一个符号与左侧的符号相同时,就会出现平凡的情况。以下是示例 2.21中的文法,它无法自上而下地解析:

A grammar is said to be left recursive if there is a nonterminal A such that A+ A α for some α.13 The trivial case occurs when the first symbol on the right-hand side of a production is the same as the symbol on the left-hand side. Here again is the grammar from Example 2.21, which cannot be parsed top-down:

         id_列表id_列表前缀

         id_listid_list_prefix ;

         id_列表_前缀id_列表_前缀, id

         id_list_prefixid_list_prefix, id

          → id

          → id

问题出在第二和第三个产生式中;在id_list_prefix解析例程中,当id为输入时,预测解析器无法判断应该使用哪个产生式。(回想一下,左递归在自下而上的语法中是可取的,因为它允许递归结构被逐步发现,如图2.15所示。)■

The problem is in the second and third productions; in the id_list_prefix parsing routine, with id on the input, a predictive parser cannot tell which of the productions it should use. (Recall that left recursion is desirable in bottom-up grammars, because it allows recursive constructs to be discovered incrementally, as in Figure 2.15.) ■

例 2.26

Example 2.26

常见前缀

Common prefixes

当两个不同的产生式具有相同的左侧,并且以相同的符号开头时,就会出现公共前缀。以下是 Algol 衍生语言中常见的示例:

Common prefixes occur when two different productions with the same left-hand side begin with the same symbol or symbols. Here is an example that commonly appears in languages descended from Algol:

        stmt → id := expr

        stmt → id := expr

         → id ( argument_list ) –– 过程调用

         → id ( argument_list ) –– procedure call

由于id位于右侧两端的开头,我们无法根据即将出现的标记在它们之间进行选择。■

With id at the beginning of both right-hand sides, we cannot choose between them on the basis of the upcoming token. ■

左递归和公共前缀都可以从语法中机械地删除。一般情况有点棘手(练习 2.25),因为预测问题可能是间接问题(例如,SA α和 A → S β,或SA αSB βA* c γ,和B* c δ)。不过,我们可以在上面的例子中看到一般的想法。

Both left recursion and common prefixes can be removed from a grammar mechanically. The general case is a little tricky (Exercise 2.25), because the prediction problem maybe an indirect one (e.g., SA α and A → S β, or SA α, SB β, A* c γ, and B* c δ). We can see the general idea in the examples above, however.

例 2.27

Example 2.27

消除左递归

Eliminating left recursion

id_list的左递归定义可以用示例 2.20中的右递归变体代替:

Our left-recursive definition of id_list can be replaced by the right-recursive variant we saw in Example 2.20:

       id_list → id id_list_tail

       id_list → id id_list_tail

       id_list_tail →,id id_list_tail

       id_list_tail →, id id_list_tail

       id_列表_尾部→;

       id_list_tail → ;

例 2.28

Example 2.28

左分解

Left factoring

我们对stmt的公共前缀定义可以通过一种称为左分解的技术变为 LL(1) :

Our common-prefix definition of stmt can be made LL(1) by a technique called left factoring:

stmt → id stmt_list_tail

stmt → id stmt_list_tail

stmt_list_tail → := expr | (参数列表)

stmt_list_tail → := expr | ( argument_list )

当然,仅仅消除左递归和公共前缀并不能保证使语法成为 LL(1)。有无数种非 LL语言(不存在 LL 语法的语言),消除左递归和公共前缀的机械转换对它们的语法非常有效。幸运的是,实践中出现的少数非 LL 语言通常可以通过用一两个简单的启发式方法增强解析算法来处理。

Of course, simply eliminating left recursion and common prefixes is not guaranteed to make a grammar LL(1). There are infinitely many non-LL languages—languages for which no LL grammar exists—and the mechanical transformations to eliminate left recursion and common prefixes work on their grammars just fine. Fortunately, the few non-LL languages that arise in practice can generally be handled by augmenting the parsing algorithm with one or two simple heuristics.

例 2.29

Example 2.29

解析“悬垂else

Parsing a “dangling else

“不完全是 LL” 构造的最著名示例出现在 Pascal 等语言中,其中if语句的else部分是可选的。自然语法片段

The best known example of a “not quite LL” construct arises in languages like Pascal, in which the else part of an if statement is optional. The natural grammar fragment

      语句→ 如果条件则子句 else子句|其他语句

      stmt → if condition then_clause else_clause | other_stmt

nbsp;     then_clause → then语句

nbsp;     then_clause → then stmt

      else_clause → else stmt | else_clause → else stmt | else_clause → else stmt | else_clause ε

      else_clause → else stmt | ε

是模棱两可的(因此既不是 LL 也不是 LR);它允许if C 1 then if C 2 then S 1 else S 2中的else与 then 配对。不太自然的语法片段

is ambiguous (and thus neither LL nor LR); it allows the else in if C1 then if C2 then S1 else S2 to be paired with either then. The less natural grammar fragment

stmt平衡_stmt |不平衡_stmt

stmtbalanced_stmt | unbalanced_stmt

平衡语句→ 如果条件平衡语句否则平衡语句|其他语句

balanced_stmt → if condition then balanced_stmt else balanced_stmt | other_stmt

unbalanced_stmt → 如果条件stmt | 如果条件平衡_stmt否则unbalanced_stmt

unbalanced_stmt → if condition then stmt | if condition then balanced_stmt else unbalanced_stmt

可以自下而上地解析,但不能自上而下地解析( Pascal else语句没有纯粹的自上而下的语法。balanced_stmt是thenelse数量相同的语句。unbalanced_stmt有多个then。■

can be parsed bottom-up but not top-down (there is no pure top-down grammar for Pascal else statements). A balanced_stmt is one with the same number of thens and elses. An unbalanced_stmt has more thens. ■

无论是自上而下还是自下而上解析,通常的方法都是将歧义语法与“消歧规则”结合使用,该规则规定,如果两个可能的产生式发生冲突,则使用在语法中文本上最先出现的那个。在上面的歧义片段中,else_clauseelse stmt位于else_clauseε之前,这一事实最终将else与最近的then配对。

The usual approach, whether parsing top-down or bottom-up, is to use the ambiguous grammar together with a “disambiguating rule,” which says that in the case of a conflict between two possible productions, the one to use is the one that occurs first, textually, in the grammar. In the ambiguous fragment above, the fact that else_clauseelse stmt comes before else_clauseε ends up pairing the else with the nearest then.

例 2.30

Example 2.30

“悬而未决的else ”程序错误

“Dangling else“ program bug

更好的是,语言设计者可以通过选择不同的语法来避免这类问题。Pascal中悬空 else问题的歧义不仅会导致解析问题,还会导致编写和维护正确的程序问题。大多数 Pascal 程序员都曾编写过类似这样的程序:

Better yet, a language designer can avoid this sort of problem by choosing different syntax. The ambiguity of the dangling else problem in Pascal leads to problems not only in parsing, but in writing and maintaining correct programs. Most Pascal programmers at one time or another ended up writing a program like this one:

如果 P <> nil 则

if P <> nil then

 如果 P^.val = 目标则

 if P^.val = goal then

  找到它 := true

  foundIt := true

别的

else

 结束列表 := true

 endOfList := true

尽管缩进,Pascal 手册指出else子句匹配最接近的未匹配的then — 在本例中是内部子句 — 这显然不是程序员的本意。为了获得所需的效果,Pascal 程序员需要编写

Indentation notwithstanding, the Pascal manual states that an else clause matches the closest unmatched then—in this case the inner one—which is clearly not what the programmer intended. To get the desired effect, the Pascal programmer needed to write

如果 P <> nil 则开始

if P <> nil then begin

 如果 P^.val = 目标则

 if P^.val = goal then

  找到它 := true

  foundIt := true

结尾

end

别的

else

 结束列表 := true

 endOfList := true

例 2.31

Example 2.31

结构化语句的结束标记

End markers for structured statements

许多其他 Algol 系列语言(包括 Modula、Modula-2 和 Oberon,这些都是 Pascal 的设计者 Niklaus Wirth 的最新发明)要求在所有结构化语句上添加明确的结束标记。Modula-2 中if语句的语法片段如下所示:

Many other Algol-family languages (including Modula, Modula-2, and Oberon, all more recent inventions of Pascal's designer, Niklaus Wirth) require explicit end markers on all structured statements. The grammar fragment for if statements in Modula-2 looks something like this:

设计与实现

Design & Implementation

2.6 悬垂else

2.6 The dangling else

语言语法的一个简单变化——消除悬垂else——不仅降低了编程错误的可能性,而且大大简化了解析。有关悬垂else问题的更多信息,请参阅练习 2.24第 6.4 节

A simple change in language syntax—eliminating the dangling else—not only reduces the chance of programming errors, but also significantly simplifies parsing. For more on the dangling else problem, see Exercise 2.24 and Section 6.4.

stmt → IF条件 then_clause else_clause END | other_stmt

stmt → IF condition then_clause else_clause END | other_stmt

then_clause → THEN stmt_list

then_clause → THEN stmt_list

else_clause → ELSE stmt_list | else_clause → ELSE stmt_list | ε

else_clause → ELSE stmt_list | ε

添加END可以消除歧义。■

The addition of the END eliminates the ambiguity. ■

Modula-2 使用END来终止其所有结构化语句。Ada 和 Fortran 77 使用 end if结束 if(使用end w hile来结束while,等等)。Algol 68 通过反向拼写初始关键字来创建其终止符(if…fi、case…esac、do…od,等等)。

Modula-2 uses END to terminate all its structured statements. Ada and Fortran 77 end an if with end if (and a while with end while, etc.). Algol 68 creates its terminators by spelling the initial keyword backward (if… fi, case… esac, do… od, etc.).

例 2.32

Example 2.32

elsif的必要性

The need for elsif

结束标记的一个问题是它们容易聚在一起。在 Pascal 中,可以写成

One problem with end markers is that they tend to bunch up. In Pascal one could write

如果 A = B 那么...

if A = B then …

否则,如果 A = C 则...

else if A = C then …

否则,如果 A = D 则...

else if A = D then …

否则,如果 A = E 则...

else if A = E then …

别的 …

else …

有了结束标记,这就变成了

With end markers this becomes

如果 A = B 那么...

if A = B then …

否则,如果 A = C 则...

else if A = C then …

否则,如果 A = D 则...

else if A = D then …

否则,如果 A = E 则...

else if A = E then …

别的 …

else …

结束 结束 结束 结束

end end end end

为了避免这种尴尬,具有结束标记的语言通常会提供一个elsif关键字(有时拼写为elif):

To avoid this awkwardness, languages with end markers generally provide an elsif keyword (sometimes spelled elif):

如果 A = B 那么...

if A = B then …

否则,如果 A = C,则…

elsif A = C then …

否则,如果 A = D,则…

elsif A = D then …

否则,如果 A = E,则…

elsif A = E then …

别的 …

else …

结尾

end

2.3.3 表驱动的自上而下的解析

2.3.3 Table-Driven Top-Down Parsing

例 2.33

Example 2.33

自上而下解析的驱动程序和表

Driver and table for top-down parsing

在递归下降解析器中,case语句的每个分支都对应一个产生式,并包含与该产生式右侧的符号相对应的解析例程和匹配调用。在解析过程中的任何给定点,如果我们考虑当前调用堆栈中解析例程调用中程序计数器以外的调用(尚未发生的调用),我们将获得解析器期望从此处到程序结束之间看到的符号列表。表驱动的自上而下的解析器维护一个包含相同符号列表的显式堆栈。

In a recursive descent parser, each arm of a case statement corresponds to a production, and contains parsing routine and match calls corresponding to the symbols on the right-hand side of that production. At any given point in the parse, if we consider the calls beyond the program counter (the ones that have yet to occur) in the parsing routine invocations currently in the call stack, we obtain a list of the symbols that the parser expects to see between here and the end of the program. A table-driven top-down parser maintains an explicit stack containing this same list of symbols.

图 2.19给出了这种解析器的伪代码。该代码与语言无关。它需要一个与语言相关的解析表,该解析表通常由自动工具生成。对于图 2.16中的计算器语法,该表如图2.20所示。■

Pseudocode for such a parser appears in Figure 2.19. The code is language independent. It requires a language-dependent parsing table, generally produced by an automatic tool. For the calculator grammar of Figure 2.16, the table appears in Figure 2.20. ■

电话02-19-9780124104099
图 2.19 表驱动 LL(I) 解析器的驱动程序。
电话02-20-9780124104099
图 2.20 计算器语言的 LL(I) 解析表。表项表示要预测的产生式(如图2.23中编号所示)。破折号表示错误。当栈顶符号是终结符时,适当的操作始终是将其与来自扫描仪的传入标记进行匹配。辅助表(此处未显示)给出了每个产生式的右侧符号。

例 2.34

Example 2.34

“求和与平均”程序的表驱动解析

Table-driven parse of the “sum and average” program

为了说明该算法,图 2.21显示了示例 2.24中的求和平均程序的堆栈和输入随时间的变化情况。解析器循环迭代,从堆栈中弹出顶部符号并执行以下操作:如果弹出的符号是终结符,解析器将尝试将其与来自扫描器的传入标记进行匹配。如果匹配失败,解析器将宣布语法错误并启动某种错误恢复(参见第 2.3.5 节)。如果弹出的符号是非终结符,解析器将使用该非终结符和下一个可用的输入标记来索引二维表,该表告诉它要预测哪个产生式(或者是否宣布语法错误并启动恢复)。

To illustrate the algorithm, Figure 2.21 shows a trace of the stack and the input over time, for the sum-and-average program of Example 2.24. The parser iterates around a loop in which it pops the top symbol off the stack and performs the following actions: If the popped symbol is a terminal, the parser attempts to match it against an incoming token from the scanner. If the match fails, the parser announces a syntax error and initiates some sort of error recovery (see Section 2.3.5). If the popped symbol is a nonterminal, the parser uses that nonterminal together with the next available input token to index into a two-dimensional table that tells it which production to predict (or whether to announce a syntax error and initiate recovery).

电话02-21-9780124104099
图 2.21 示例 2.24中的求和与平均程序的表驱动 LL(1) 解析的轨迹。

最初,解析堆栈包含语法的起始符号(在我们的例子中是程序)。当它预测一个产生式时,解析器会以相反的顺序将右侧符号推送到解析堆栈上,因此这些符号中的第一个符号最终位于堆栈顶部。当我们匹配结束标记标记$$时,解析成功完成。假设$$只在语法中出现一次,在第一个产生式的末尾,并且扫描器只在文件末尾返回此标记,则任何语法错误都一定会表现为失败的匹配表中的错误条目。■

Initially, the parse stack contains the start symbol of the grammar (in our case, program). When it predicts a production, the parser pushes the right-hand-side symbols onto the parse stack in reverse order, so the first of those symbols ends up at top-of-stack. The parse completes successfully when we match the end marker token, $$. Assuming that $$ appears only once in the grammar, at the end of the first production, and that the scanner returns this token only at end-of-file, any syntax error is guaranteed to manifest itself either as a failed match or as an error entry in the table. ■

正如我们在第 2.3.1 节末尾所暗示的,预测集是用称为 FIRST 和 FOLLOW 的更简单的集合来定义的,其中 FIRST( A ) 是所有可以作为A开头的标记的集合,而 FOLLOW( A ) 是某个有效程序中可以跟在A之后的所有标记的集合。如果我们以显而易见的方式扩展 FIRST 的定义域以包括符号字符串,那么我们说产生式Aβ的预测集是 FIRST ( β ),加上如果β* ε 的FOLLOW( A ) 。为了符号方便,我们定义谓词 EPS 使得 EPS( β ) ≡ β* ε

As we hinted at the end of Section 2.3.1, predict sets are defined in terms of simpler sets called FIRST and FOLLOW, where FIRST(A) is the set of all tokens that could be the start of an A and FOLLOW(A) is the set of all tokens that could come after an A in some valid program. If we extend the domain of FIRST in the obvious way to include strings of symbols, we then say that the predict set of a production Aβ is FIRST (β), plus FOLLOW(A) if β* ε. For notational convenience, we define the predicate EPS such that EPS(β) ≡ β* ε.

例 2.35

Example 2.35

计算器语言的预测集

Predict sets for the calculator language

我们可以用计算器语法(图 2.16 )来说明构造这些集合的算法。我们从语法的“明显”事实开始,然后以此为基础进行归纳构建。如果我们用普通的 BNF(没有 EBNF '|' 构造)重写该语法,那么它有 19 个产生式。这些“明显”的事实来自右侧相邻的符号对。在第一个产生式中,我们可以看到$$ ∈ FOLLOW( stmt_list )。在第三个产生式(stmt_listε)中,EPS( stmt_list ) = true。在第四个产生式(stmtid := expr)中,id ∈ FIRST( stmt )(同样 := ∈ FOLLOW( id ),但事实证明非终结符不需要 FOLLOW 集)。在第五个和第六个产生式(stmtread id | write expr)中,{ read , write } ⊂ FIRST( stmt )。“显而易见”的事实的完整集合如图2.22所示。

We can illustrate the algorithm to construct these sets using our calculator grammar (Figure 2.16). We begin with “obvious” facts about the grammar and build on them inductively. If we recast the grammar in plain BNF (no EBNF '|' constructs), then it has 19 productions. The “obvious” facts arise from adjacent pairs of symbols in right-hand sides. In the first production, we can see that $$ ∈ FOLLOW(stmt_list). In the third (stmt_listε), EPS(stmt_list) = true. In the fourth production (stmtid := expr), id ∈ FIRST(stmt) (also := ∈ FOLLOW(id), but it turns out we don't need FOLLOW sets for nonterminals). In the ifth and sixth productions (stmtread id | write expr), {read, write} ⊂ FIRST(stmt). The complete set of “obvious” facts appears in Figure 2.22.

电话02-22-9780124104099
图 2.22 关于 LL(I) 计算器语法(左)的“明显”事实(右)。

从“显而易见”的事实中,我们可以在第二次遍历语法时推断出更大的事实集合。例如,在第二个产生式(stmt_liststmt stmt_list)中,我们可以推断出 { id , read , write } ⊂ FIRST( stmt_list ),因为我们已经知道 { id , read , write } ⊂ FIRST( stmt ),并且stmt_list可以从stmt开始。类似地,在第一个产生式中,我们可以推断出$$ ∈ FIRST( program ),因为我们已经知道 EPS( stmt_list ) = true

From the “obvious” facts we can deduce a larger set of facts during a second pass over the grammar. For example, in the second production (stmt_liststmt stmt_list) we can deduce that {id, read, write} ⊂ FIRST(stmt_list), because we already know that {id, read, write} ⊂ FIRST(stmt), and a stmt_list can begin with a stmt. Similarly, in the first production, we can deduce that $$ ∈ FIRST(program), because we already know that EPS(stmt_list) = true.

设计与实现

Design & Implementation

2.7 递归下降和表驱动 LL 解析

2.7 Recursive descent and table-driven LL parsing

当试图理解递归下降和表驱动 LL 解析之间的联系时,很容易想象表驱动解析器的显式堆栈反映了递归下降解析器的隐式调用堆栈,但事实并非如此。

When trying to understand the connection between recursive descent and table-driven LL parsing, it is tempting to imagine that the explicit stack of the table-driven parser mirrors the implicit call stack of the recursive descent parser, but this is not the case.

可视化两种自上而下解析实现的更好方法是记住它们都是通过深度优先从左到右的遍历来发现解析树。当我们处于解析中的给定点(例如此处显示的树中带圆圈的节点)时,递归下降解析器的隐式调用堆栈会为返回根路径上的每个节点保留一个框架,该框架是在调用与该节点对应的例程时创建的。(此路径显示为灰色。)

A better way to visualize the two implementations of top-down parsing is to remember that both are discovering a parse tree via depth-first left-to-right traversal. When we are at a given point in the parse—say the circled node in the tree shown here—the implicit call stack of a recursive descent parser holds a frame for each of the nodes on the path back to the root, created when the routine corresponding to that node was called. (This path is shown in grey.)

u02-02-9780124104099

但这些节点并不重要。对于其余的解析来说,重要的是(如这里的白色路径所示)对递归下降例程的 case 语句臂的即将到来的调用。这些调用(这些解析树节点)正是表驱动 LL 解析器的显式堆栈的内容。

But these nodes are immaterial. What matters for the rest of the parse—as shown on the white path here—are the upcoming calls on the case statement arms of the recursive descent routines. Those calls—those parse tree nodes—are precisely the contents of the explicit stack of a table-driven LL parser.

在第十一个产生式(factor_tailmult_op factor factor_tail)中,我们可以推出 {(, id , number } ⊂ FOLLOW( mult_op ),因为我们已经知道 {(, id , number } ⊂ FIRST( factor ),并且factor在右侧跟在mult_op后面。在产生式exprterm term_tail中,我们可以推出) ∈ FOLLOW( term_tail ),因为我们已经知道) ∈ FOLLOW( expr ),并且term_tail可以是expr的最后一部分。在同一个产生式中,我们还可以推出) ∈ FOLLOW( term ),因为term_tail可以生成ε (EPS( term_tail ) = true ),从而允许term成为expr的最后一部分。

In the eleventh production (factor_tailmult_op factor factor_tail), we can deduce that {(, id, number} ⊂ FOLLOW(mult_op), because we already know that {(, id, number} ⊂ FIRST(factor), and factor follows mult_op in the right-hand side. In the production exprterm term_tail, we can deduce that) ∈ FOLLOW(term_tail), because we already know that ) ∈ FOLLOW(expr), and a term_tail can be the last part of an expr. In this same production, we can also deduce that) ∈ FOLLOW(term), because the term_tail can generate ε (EPS(term_tail) = true), allowing a term to be the last part of an expr.

我们可以从第二遍语法中学到更多东西,但上面的例子涵盖了所有不同类型的情况。为了完成我们的计算,我们继续对语法进行额外的遍历,直到我们不再学到任何东西(即,我们不向任何 FIRST 和 FOLLOW 集添加任何内容)。然后我们构造 PREDICT 集。所有三个集合的最终版本如图2.23所示。图 2.20的解析表直接来自 PREDICT。■

There is more that we can learn from our second pass through the grammar, but the examples above cover all the different kinds of cases. To complete our calculation, we continue with additional passes over the grammar until we don't learn any more (i.e., we don't add anything to any of the FIRST and FOLLOW sets). We then construct the PREDICT sets. Final versions of all three sets appear in Figure 2.23. The parse table of Figure 2.20 follows directly from PREDICT. ■

电话02-23-9780124104099
图 2.23 计算器语言的 FIRST、FOLLOW 和 PREDICT 集。FIRST(c) = {c} ∀ 标记 c。EPS( A ) 为真当且仅当A ∈ { stmt_list, term_tail, factor_tail }

图 2.24中更正式地给出了计算 EPS、FIRST、FOLLOW 和 PREDICT 集的算法。它依赖于以下定义:

The algorithm to compute EPS, FIRST, FOLLOW, and PREDICT sets appears, a bit more formally, in Figure 2.24. It relies on the following definitions:

电话02-24-9780124104099
图 2.24 计算 FIRST、FOLLOW 和 PREDICT 集的算法。当且仅当具有相同左侧的产生式的所有 PREDICT 集不相交时,语法才是 LL(1)。

EPS( α ) == 如果α* ε则为真,否则为假

EPS(α) ≡ if α* ε then true else false

FIRST( α ) ≡ { c : α⇒ *}

FIRST(α) ≡ {c : α* c β }

FOLLOW( A ) ≡ {c : S ⇒ + α A c β }

FOLLOW(A) ≡ {c : S ⇒+ α A c β }

PREDICT( Aα ) ≡ FIRST( α ) ∪ (如果 EPS( α ) 则 FOLLOW( A ) 否则 ∅)

PREDICT(Aα) ≡ FIRST(α) ∪ ( if EPS(α) then FOLLOW(A) else ∅)

PREDICT 的定义假设该语言已经增加了结束标记,即 FOLLOW( S ) = {$$}。请注意,长度大于 1 的字符串的 FIRST 集和 EPS 值是根据需要计算的;它们是不显式存储。算法保证终止(即收敛到一个解决方案),因为 FIRST 和 FOLLOW 集的大小受语法中终端数量的限制。

The definition of PREDICT assumes that the language has been augmented with an end marker—that is, that FOLLOW(S) = {$$}. Note that FIRST sets and EPS values for strings of length greater than one are calculated on demand; they are not stored explicitly. The algorithm is guaranteed to terminate (i.e., converge on a solution), because the sizes of the FIRST and FOLLOW sets are bounded by the number of terminals in the grammar.

如果在计算 PREDICT 集的过程中,我们发现某个标记属于多个具有相同左侧的产生式的 PREDICT 集,则该语法不是 LL(1),因为当左侧位于解析堆栈的顶部(或者我们在递归下降解析器中左侧的子例程中)并且我们看到该标记出现在输入中时,我们将无法选择使用哪个产生式。这种歧义称为预测-预测冲突;它可能是因为同一个标记可以开始多个右侧,或者因为它可以开始一个右侧并且也可以出现在某个有效程序的左侧之后,并且一个可能的右侧可以生成ε而产生的。

If in the process of calculating PREDICT sets we find that some token belongs to the PREDICT set of more than one production with the same left-hand side, then the grammar is not LL(1), because we will not be able to choose which of the productions to employ when the left-hand side is at the top of the parse stack (or we are in the left-hand side's subroutine in a recursive descent parser) and we see the token coming up in the input. This sort of ambiguity is known as a predict-predict conflict; it can arise either because the same token can begin more than one right-hand side, or because it can begin one right-hand side and can also appear after the left-hand side in some valid program, and one possible right-hand side can generate ε.

02-01-9780124104099检查你的理解

Check Your Understanding

29. 描述上下文无关文法中两个无法自上而下解析的常见习语。

29. Describe two common idioms in context-free grammars that cannot be parsed top-down.

30. 什么是“悬垂else ”问题?现代语言中如何避免这个问题?

30. What is the “dangling else“ problem? How is it avoided in modern languages?

31. 讨论递归下降和表驱动自上而下解析之间的异同。

31. Discuss the similarities and differences between recursive descent and table-driven top-down parsing.

32.  FIRST 和 FOLLOW 集是什么?它们有什么用途?

32. What are FIRST and FOLLOW sets? What are they used for?

33. 在什么情况下,自上而下的解析器会预测产生式Aα

33. Under what circumstances does a top-down parser predict the production Aα?

34. 哪些“明显的”事实构成了 FIRST 集和 FOLLOW 集构建的基础?

34. What sorts of “obvious” facts form the basis of FIRST set and FOLLOW set construction?

35. 概述用于完成 FIRST 和 FOLLOW 集构建的算法。我们如何知道何时完成?

35. Outline the algorithm used to complete the construction of FIRST and FOLLOW sets. How do we know when we are done?

36. 我们如何知道一个语法不是LL(1)的?

36. How do we know when a grammar is not LL(1)?

2.3.4 自底向上解析

2.3.4 Bottom-Up Parsing

从概念上讲,正如我们在2.3 节开头看到的那样,自下而上的解析器通过维护解析树的部分完成的子树森林来工作,每当它识别出输入字符串最右侧派生中使用的某些产生式右侧的符号时,它就会将这些子树连接在一起。它会创建一个新的内部节点,并将连接在一起的树的根节点作为该节点的子节点。

Conceptually, as we saw at the beginning of Section 2.3, a bottom-up parser works by maintaining a forest of partially completed subtrees of the parse tree, which it joins together whenever it recognizes the symbols on the right-hand side of some production used in the right-most derivation of the input string. It creates a new internal node and makes the roots of the joined-together trees the children of that node.

实际上,自下而上的解析器几乎总是表驱动的。它将部分完成的子树的根保存在堆栈中。当它从扫描器标记移入堆栈。当它识别出堆栈顶部的几个符号构成右侧时,它会通过弹出堆栈并将左侧推入其位置来将这些符号缩减到其左侧。堆栈的作用是自上而下和自下而上解析之间的第一个重要区别:自上而下的解析器的堆栈包含解析器期望在未来看到的内容的列表;自下而上的解析器的堆栈包含解析器过去已经看到的内容的记录。

In practice, a bottom-up parser is almost always table-driven. It keeps the roots of its partially completed subtrees on a stack. When it accepts a new token from the scanner, it shifts the token into the stack. When it recognizes that the top few symbols on the stack constitute a right-hand side, it reduces those symbols to their left-hand side by popping them off the stack and pushing the left-hand side in their place. The role of the stack is the first important difference between top-down and bottom-up parsing: a top-down parser's stack contains a list of what the parser expects to see in the future; a bottom-up parser's stack contains a record of what the parser has already seen in the past.

规范推导

Canonical Derivations

例 2.36

Example 2.36

导出id 列表

Derivation of an id list

我们之前还提到,自下而上的解析器的操作会反向追踪最右侧(规范)推导。从左到右的部分子树的根与剩余的输入一起构成最右侧推导的句子形式。例如,在图 2.14的右侧,我们有以下一系列步骤:

We also noted earlier that the actions of a bottom-up parser trace out a right-most (canonical) derivation in reverse. The roots of the partial subtrees, left-to-right, together with the remaining input, constitute a sentential form of the right-most derivation. On the right-hand side of Figure 2.14, for example, we have the following series of steps:

堆栈内容(部分树的根)剩余输入
εA、B、C;
id(A),B,C;
id(A),B、C;
id(A),id(B),C;
id(A),id(B),碳;
id(A)、id(B)、id(C)
id(A),id(B),id(C)
id(A),id(B),id(C)id_list_tail
id(A),id(B)id_list_tail
id (A) id_list_tail
id_列表

最后四行(不仅仅是将标记移入森林的行)对应于最右边的推导:

The last four lines (the ones that don't just shift tokens into the forest) correspond to the right-most derivation:

id_list⇒id id_list_tail

id_list ⇒ id id_list_tail

 ⇒ id,id id_list_tail

 ⇒ id, id id_list_tail

 ⇒ id,id,id id_list_tail

 ⇒ id, id, id id_list_tail

 ⇒ id,id,id;

 ⇒ id, id, id ;

在解析的每一步中需要连接在一起以表示向后推导的下一步的符号称为句柄在上面的解析轨迹中,句柄被加了下划线。■

The symbols that need to be joined together at each step of the parse to represent the next step of the backward derivation are called the handle of the sentential form. In the parse trace above, the handles are underlined. ■

例 2.37

Example 2.37

计算器语言的自下而上的语法

Bottom-up grammar for the calculator language

在我们的id_list示例中,直到将整个输入移到堆栈上时才找到句柄。一般来说情况并非如此。我们可以通过检查计算器语言的 LR 版本来获得更现实的示例,如图2.25所示。虽然图 2.16的 LL 语法可以自下而上地进行解析,但是图 2.25中的版本更可取,原因有二。首先,它对stmt_list使用左递归产生式。左递归允许解析器在进行过程中折叠长语句列表,而不是等到整个列表在堆栈上然后从末尾折叠它。其次,它对exprterm使用左递归产生式。这些产生式捕获左结合性,同时仍将运算符及其操作数保持在同一右侧,这是我们在自上而下的语法中无法做到的。■

In our id_list example, no handles were found until the entire input had been shifted onto the stack. In general this will not be the case. We can obtain a more realistic example by examining an LR version of our calculator language, shown in Figure 2.25. While the LL grammar of Figure 2.16 can be parsed bottom-up, the version in Figure 2.25 is preferable for two reasons. First, it uses a left-recursive production for stmt_list. Left recursion allows the parser to collapse long statement lists as it goes along, rather than waiting until the entire list is on the stack and then collapsing it from the end. Second, it uses left-recursive productions for expr and term. These productions capture left associativity while still keeping an operator and its operands together in the same right-hand side, something we were unable to do in a top-down grammar. ■

电话02-25-9780124104099
图 2.25 计算器语言的 LR(I) 语法。已对产品进行编号,以便在未来的图表中参考。

使用 LR 项目建模解析

Modeling a Parse with LR Items

例 2.38

Example 2.38

自下而上解析“求和与平均”程序

Bottom-up parse of the “sum and average” program

假设我们要解析示例 2.24中的求和与平均值程序:

Suppose we are to parse the sum-and-average program from Example 2.24:

读A

read A

读B

read B

总和 := A + B

sum := A + B

写总和

write sum

写出总和 / 2

write sum / 2

成功的关键在于弄清楚我们何时到达右侧的末尾 — 即,何时我们在解析堆栈的顶部有一个句柄。诀窍是跟踪我们可能在任何特定时间“处于”的产生式集合,以及我们可能处于这些产生式的哪个位置的指示。

The key to success will be to figure out when we have reached the end of a right-hand side—that is, when we have a handle at the top of the parse stack. The trick is to keep track of the set of productions we might be “in the middle of” at any particular time, together with an indication of where in those productions we might be.

当我们开始执行时,解析堆栈是空的,我们正处于程序的产生式的开头。(一般来说,我们可以假设只有一个产生式,其起始符号在左侧;很容易修改任何语法都可以做到这一点。)我们可以在产生式的右侧用 • 来表示我们的位置——更具体地说,是解析堆栈顶部所表示的位置:

When we begin execution, the parse stack is empty and we are at the beginning of the production for program. (In general, we can assume that there is only one production with the start symbol on the left-hand side; it is easy to modify any grammar to make this the case.) We can represent our location—more specifically, the location represented by the top of the parse stack—with a • in the right-hand side of the production:

程序 → • stmt_list $$

program → • stmt_list $$

当一个产生式被添加一个 • 时,它就被称为 LR。由于该项中的 • 紧挨着一个非终结符(即stmt_list )前面,我们可能即将看到该非终结符在输入中出现。这种可能性意味着我们可能处于某个产生式的开头,其左侧是stmt_list :

When augmented with a •, a production is called an LR item. Since the • in this item is immediately in front of a nonterminal—namely stmt_list—we may be about to see the yield of that nonterminal coming up on the input. This possibility implies that we may be at the beginning of some production with stmt_list on the left-hand side:

程序→ • stmt_list $$

program → • stmt_list $$

stmt_list → • stmt_list stmt

stmt_list → • stmt_list stmt

stmt_list → • stmt

stmt_list → • stmt

并且,由于stmt是非终结符,我们也可能位于任何左侧是stmt的产生式的开头:

And, since stmt is a nonterminal, we may also be at the beginning of any production whose left-hand side is stmt:

(状态 0)

(State 0)

程序→ • stmt_list $$

program → • stmt_list $$

stmt_list → • stmt_list stmt

stmt_list → • stmt_list stmt

stmt_list → • stmt

stmt_list → • stmt

stmt → • id := expr

stmt → • id := expr

stmt → • 读取 id

stmt → • read id

stmt → • 写入expr

stmt → • write expr

由于所有这些最后的产生式都以终结符开头,因此不需要在列表中添加任何附加项。原始项(program → • stmt_list $$)称为列表的基础。附加项是它的闭包。列表表示解析器的初始状态。当我们移位和归约时,项集将发生变化,始终指示哪些产生式可能是接下来在输入字符串的推导中使用的正确产生式。如果我们到达某个项在右侧末尾有 • 的状态,我们可以通过该产生式来归约。否则,像当前情况一样,我们必须移位。请注意,如果我们需要移位,但传入的标记不能跟在当前状态的任何项中的 • 之后,则发生了语法错误。我们将在C-2.3.5 节中更详细地考虑错误恢复。

Since all of these last productions begin with a terminal, no additional items need to be added to our list. The original item (program → • stmt_list $$) is called the basis of the list. The additional items are its closure. The list represents the initial state of the parser. As we shift and reduce, the set of items will change, always indicating which productions may be the right one to use next in the derivation of the input string. If we reach a state in which some item has the • at the end of the right-hand side, we can reduce by that production. Otherwise, as in the current situation, we must shift. Note that if we need to shift, but the incoming token cannot follow the • in any item of the current state, then a syntax error has occurred. We will consider error recovery in more detail in Section C-2.3.5.

我们即将收到的 token 是read。一旦将其移入堆栈,我们就知道我们处于以下状态:

Our upcoming token is a read. Once we shift it onto the stack, we know we are in the following state:

(状态 1)

(State 1)

stmt → 读取 • id

stmt → read • id

此状态具有单个基础项和一个空闭包——• 位于终端之前。在移动 A 之后我们有

This state has a single basis item and an empty closure—the • precedes a terminal. After shifting the A, we have

(状态 1')

(State 1')

stmt → 读取 ID •

stmt → read id •

我们现在知道read id是句柄,我们必须进行归约。归约会从解析堆栈中弹出两个符号,并在其位置推送一个stmt,但新状态应该是什么?如果我们想象回到我们移动read(右侧的第一个符号)的时间点,我们可以看到答案。当时我们处于上面标记为“状态 0”的状态,输入中即将到来的标记(尽管我们当时没有查看它们)是read id。我们现在已经使用了这些标记,并且我们知道它们构成了一个stmt。通过将stmt推送到堆栈上,我们本质上已经用stmt替换了输入流上的read id,然后将非终结符(而不是其产量)“移入”堆栈。由于状态 0 中的一项是

We now know that read id is the handle, and we must reduce. The reduction pops two symbols off the parse stack and pushes a stmt in their place, but what should the new state be? We can see the answer if we imagine moving back in time to the point at which we shifted the read—the first symbol ofthe right-hand side. At that time we were in the state labeled “State 0” above, and the upcoming tokens on the input (though we didn't look at them at the time) were read id. We have now consumed these tokens, and we know that they constituted a stmt. By pushing a stmt onto the stack, we have in essence replaced read id with stmt on the input stream, and have then “shifted” the nonterminal, rather than its yield, into the stack. Since one of the items in State 0 was

stmt_list → • stmt

stmt_list → • stmt

我们现在有

we now have

(状态 0')

(State 0')

stmt_liststmt

stmt_liststmt

我们必须再次进行归约。我们从堆栈中删除stmt ,并在其位置推送一个stmt_list 。我们可以再次将此视为在状态 0 中“移动” stmt_list。由于状态 0 中的两个项目在 • 之后有一个stmt_list,因此我们不知道(不向前看)哪个产生式将是下一个用于推导的产生式,但我们不必知道。自下而上解析相对于自上而下解析的主要优势在于,我们不需要提前预测我们将扩展哪个产生式。

Again we must reduce. We remove the stmt from the stack and push a stmt_list in its place. Again we can see this as “shifting” a stmt_list when in State 0. Since two of the items in State 0 have a stmt_list after the •, we don't know (without looking ahead) which of the productions will be the next to be used in the derivation, but we don't have to know. The key advantage of bottom-up parsing over top-down parsing is that we don't need to predict ahead of time which production we shall be expanding.

我们的新状态如下:

Our new state is as follows:

(状态 2)

(State 2)

程序stmt_list • $$

programstmt_list • $$

stmt_liststmt_liststmt

stmt_liststmt_liststmt

stmt → • id := expr

stmt → • id := expr

stmt → • 读取 id

stmt → • read id

stmt → • 写入expr

stmt → • write expr

前两个产生式是基础;其他产生式是闭包。由于没有项目以 • 结尾,我们移动下一个标记,该标记又恰好是 read 将我们带回到状态 1。移动B再次将我们带回到状态 1',此时我们进行归约。不过,这一次,我们在移动左侧stmt之前返回状态 2 而不是状态 0。为什么?因为当我们开始读取右侧时,我们处于状态 2。■

The first two productions are the basis; the others are the closure. Since no item has a • at the end, we shift the next token, which happens again to be a read, taking us back to State 1. Shifting the B takes us to State 1' again, at which point we reduce. This time however, we go back to State 2 rather than State 0 before shifting the left-hand-side stmt. Why? Because we were in State 2 when we began to read the right-hand side. ■

特征有限状态机和 LR 解析变体

The Characteristic Finite-State Machine and LR Parsing Variants

LR 系列解析器通过将已遍历的状态与语法符号一起推送到解析堆栈中来跟踪它们。事实上,驱动解析算法的是状态(而不是符号):它们告诉我们在右侧开始时我们处于什么状态。具体来说,当状态和输入的组合告诉我们需要使用产生式Aα进行归约时,我们会从堆栈中弹出长度α)个符号,以及我们移动的状态记录在移动这些符号时,这些弹出窗口会显示我们在移动之前的状态,这样我们就可以返回到该状态并继续操作,就像我们一开始就看到了A一样。

An LR-family parser keeps track of the states it has traversed by pushing them into the parse stack, along with the grammar symbols. It is in fact the states (rather than the symbols) that drive the parsing algorithm: they tell us what state we were in at the beginning of a right-hand side. Specifically, when the combination of state and input tells us we need to reduce using production Aα, we pop length(α) symbols off the stack, together with the record of states we moved through while shifting those symbols. These pops expose the state we were in immediately prior to the shifts, allowing us to return to that state and proceed as if we had seen A in the first place.

我们可以将 LR 系列解析器的移位规则视为有限自动机的转换函数,与我们用来建模扫描仪的自动机非常相似。自动机的每个状态都对应于一个项目列表,这些项目指示解析器在解析过程中的某个特定点可能处于的位置。输入符号X(可能是终结符或非终结符)的转换移动到一个状态,该状态的基础由右侧的X上的 • 已移动的项目以及需要添加为闭包的任何项目组成。这些列表由自下而上的解析器生成器构建,以构建自动机,但在解析过程中不需要。

We can think of the shift rules of an LR-family parser as the transition function of a finite automaton, much like the automata we used to model scanners. Each state of the automaton corresponds to a list of items that indicate where the parser might be at some specific point in the parse. The transition for input symbol X (which may be either a terminal or a nonterminal) moves to a state whose basis consists of items in which the • has been moved across an X in the right-hand side, plus whatever items need to be added as closure. The lists are constructed by a bottom-up parser generator in order to build the automaton, but are not needed during parsing.

事实证明,LR 系列解析器中较简单的成员 LR(0)、SLR(1) 和 LALR(1) 都使用相同的自动机,称为特征有限状态机(CFSM)。完整的 LR 解析器使用具有(对于大多数语法)更多状态的机器。这些算法之间的差异在于它们如何处理包含移位-归约冲突的状态- 一个项的 • 位于终结符前面(表明需要移位),另一个项的 • 位于右侧末尾(表明需要归约)。LR(0) 解析器仅在没有这样的状态时工作。可以证明,通过添加结束标记(即$$),任何可以自下而上确定性解析的语言都具有 LR(0) 语法。不幸的是,实际编程语言的 LR(0) 语法往往非常大且不直观。

It turns out that the simpler members of the LR family of parsers—LR(0), SLR(1), and LALR(1)—all use the same automaton, called the characteristic finite-state machine, or CFSM. Full LR parsers use a machine with (for most grammars) a much larger number of states. The differences between the algorithms lie in how they deal with states that contain a shift-reduce conflict—one item with the • in front of a terminal (suggesting the need for a shift) and another with the • at the end of the right-hand side (suggesting the need for a reduction). An LR(0) parser works only when there are no such states. It can be proven that with the addition of an end-marker (i.e., $$), any language that can be deterministically parsed bottom-up has an LR(0) grammar. Unfortunately, the LR(0) grammars for real programming languages tend to be prohibitively large and unintuitive.

SLR(简单 LR)解析器会查看即将到来的输入并使用 FOLLOW 集来解决冲突。仅当即将到来的标记在 FOLLOW( α ) 中时,SLR 解析器才会通过Aα调用约简。但是,如果标记也位于状态其他项中 • 后面的任何符号的 FIRST 集中,则它仍然会看到冲突。事实证明,在重要的情况下,标记可能在有效程序的某个地方跟随给定的非终结符,但永远不会在当前状态描述的上下文中出现。对于这些情况,全局 FOLLOW 集过于粗糙。LALR(前瞻 LR)解析器通过使用局部(特定于状态的)前瞻来改进 SLR。

SLR (simple LR) parsers peek at upcoming input and use FOLLOW sets to resolve conflicts. An SLR parser will call for a reduction via Aα only if the upcoming token(s) are in FOLLOW(α). It will still see a conflict, however, if the tokens are also in the FIRST set of any of the symbols that follow a • in other items of the state. As it turns out, there are important cases in which a token may follow a given nonterminal somewhere in a valid program, but never in a context described by the current state. For these cases global FOLLOW sets are too crude. LALR (look-ahead LR) parsers improve on SLR by using local (state-specific) look-ahead instead.

当同一组项目可能出现在 CFSM 中的两条不同路径上时,LALR 解析器中仍会出现冲突。两条路径最终都会处于同一状态,此时特定于状态的前瞻无法再区分它们。完整的 LR 解析器会复制状态,以便在它们的本地前瞻不同时保持路径不相交。

Conflicts can still arise in an LALR parser when the same set of items can occur on two different paths through the CFSM. Both paths will end up in the same state, at which point state-specific look-ahead can no longer distinguish between them. A full LR parser duplicates states in order to keep paths disjoint when their local look-aheads are different.

LALR 解析器是实践中最常见的自下而上的解析器。它们的大小和速度与 SLR 解析器相同,但能够解决更多冲突。实际编程语言的完整 LR 解析器往往非常大。一些研究人员已经开发出减少完整 LR 表大小的技术,但 LALR 在实践中运行良好,通常不需要完整 LR 的额外复杂性。Yacc /bison为 LALR 解析器生成 C 代码。

LALR parsers are the most common bottom-up parsers in practice. They are the same size and speed as SLR parsers, but are able to resolve more conflicts. Full LR parsers for real programming languages tend to be very large. Several researchers have developed techniques to reduce the size of full-LR tables, but LALR works sufficiently well in practice that the extra complexity of full LR is usually not required. Yacc/bison produces C code for an LALR parser.

自下而上解析表

Bottom-Up Parsing Tables

与表驱动的 LL(1) 解析器一样,SLR(1)、LALR(1) 或 LR(1) 解析器执行循环,在该循环中反复检查二维表以找出要采取的操作。但是,LR 系列解析器不使用当前输入标记和堆栈顶部的非终结符来索引表,而是使用当前输入标记和当前解析器状态(可在堆栈顶部找到)。“Shift”表条目表示应推送的状态。“Reduce”表条目表示应弹出的状态数和应推回到输入流中的非终结符,以便按弹出未覆盖的状态进行移动。对于减少产生式右侧的每个符号,始终有一个弹出状态。可以使用未覆盖的状态和新识别的非终结符索引表来找到下一个要推送的状态。

Like a table-driven LL(1) parser, an SLR(1), LALR(1), or LR(1) parser executes a loop in which it repeatedly inspects a two-dimensional table to find out what action to take. However, instead of using the current input token and top-of-stack nonterminal to index into the table, an LR-family parser uses the current input token and the current parser state (which can be found at the top of the stack). “Shift” table entries indicate the state that should be pushed. “Reduce” table entries indicate the number of states that should be popped and the nonterminal that should be pushed back onto the input stream, to be shifted by the state uncovered by the pops. There is always one popped state for every symbol on the right-hand side of the reducing production. The state to be pushed next can be found by indexing into the table using the uncovered state and the newly recognized nonterminal.

例 2.39

Example 2.39

自下而上的计算器语法的 CFSM

CFSM for the bottom-up calculator grammar

图 2.26显示了自下而上版本的计算器语法的 CFSM 。状态 6、7、9 和 13 包含潜在的移位-归约冲突,但所有这些冲突都可以通过全局 FOLLOW 集解决。因此,SLR 解析就足够了。例如,在状态 6 中,FIRST( add_op )∩FOLLOW( stmt )=∅。除了移位和归约规则之外,我们还允许解析表作为一种优化来包含“移位然后归约”形式的规则。此优化用于消除示例 2.38中的简单状态,例如 1' 和 0' ,它们只有一个项,末尾带有 •。

The CFSM for our bottom-up version of the calculator grammar appears in Figure 2.26. States 6, 7, 9, and 13 contain potential shift-reduce conflicts, but all of these can be resolved with global FOLLOW sets. SLR parsing therefore suffices. In State 6, for example, FIRST(add_op) ∩ FOLLOW(stmt) = ∅. In addition to shift and reduce rules, we allow the parse table as an optimization to contain rules of the form “shift and then reduce.” This optimization serves to eliminate trivial states such as 1' and 0' in Example 2.38, which had only a single item, with the • at the end.

传真:02-26-9780124104099

电话02-31-9780124104099
图 2.26 计算器语法的 CFSM图 2.25)。每个状态中的基础和闭包项由水平线分隔。通过使用“移位和归约”转换,消除了简单的仅归约状态。

图 2.27是 CFSM 的图形表示。图 2.28是适合在表驱动解析器中使用的表格表示。图 2.29是(语言无关的)解析器驱动程序的伪代码。图 2.30是解析器对求和平均程序的操作轨迹。■

A pictorial representation of the CFSM appears in Figure 2.27. A tabular representation, suitable for use in a table-driven parser, appears in Figure 2.28. Pseudocode for the (language-independent) parser driver appears in Figure 2.29. A trace of the parser's actions on the sum-and-average program appears in Figure 2.30. ■

传真:02-27-9780124104099
图 2.27 图 2.26中 CFSM 的图形表示。未显示减少操作。
电话02-28-9780124104099
图 2.28 计算器语言的 SLR(I) 解析表。表项指示是否要移位(s)、归约(r)还是先移位再归约(b)。附带的数字是移位时的新状态,或者在(移位和)归约时已识别的产生式。产生式编号如图2.25所示。为方便格式化,符号名称已缩写。破折号表示错误。辅助表(未在此处显示)给出了每个产生式左侧的符号和右侧的长度。
电话02-29-9780124104099
图 2.29 表驱动的 SLR(I) 解析器的驱动程序。我们直接调用扫描器,而不是使用图 2.172.19中的全局 input_token ,这样我们就可以将 cur_sym 设置为任意符号。我们向 pop() 例程传递一个参数,该参数指示要从堆栈中删除的符号数。
电话02-30-9780124104099
图 2.30 求和平均程序的表驱动 SLR(I) 解析的轨迹。解析堆栈中的状态以粗体显示。解析堆栈中的符号仅用于清晰显示;解析算法不需要它们。解析从堆栈中 CFSM (状态 0)的初始状态开始。当我们通过程序stmt_list $$进行归约时,解析结束,再次发现状态 0 并将程序推送到输入流。

处理 Epsilon 作品

Handling Epsilon Productions

例 2.40

Example 2.40

自下而上的计算器语法中的 Epsilon 产生式

Epsilon productions in the bottom-up calculator grammar

细心的读者可能已经注意到,除了对stmt_list、exprterm使用左递归规则之外,图 2.25中的文法还与图 2.16中的文法在另一个方面有所不同:它将stmt_list定义为一个或多个stmts的序列,而不是零个或多个。(这当然意味着它定义了一种不同的语言。)为了表示与图 2.16相同的语言,图 2.25中的产生式 3 ,

The careful reader may have noticed that the grammar of Figure 2.25, in addition to using left-recursive rules for stmt_list, expr, and term, differs from the grammar of Figure 2.16 in one other way: it defines a stmt_list to be a sequence of one or more stmts, rather than zero or more. (This means, of course, that it defines a different language.) To capture the same language as Figure 2.16, production 3 in Figure 2.25,

stmt_liststmt

stmt_liststmt

需要替换为

would need to be replaced with

stmt_listε

stmt_listε

请注意,通常情况下,语句列表为空是有意义的。在计算器语言中,它只允许一个空程序,这确实很愚蠢。然而,在实际语言中,它允许结构化语句的主体为空,这可能非常有用。人们经常希望 case或多if…then…else语句的一个分支为空,而空的while循环允许并行程序(或操作系统)等待来自另一个进程或 I/O 设备的信号。

Note that it does in general make sense to have an empty statement list. In the calculator language it simply permits an empty program, which is admittedly silly. In real languages, however, it allows the body of a structured statement to be empty, which can be very useful. One frequently wants one arm of a case or multi-way if… then … else statement to be empty, and an empty while loop allows a parallel program (or the operating system) to wait for a signal from another process or an I/O device.

例 2.41

Example 2.41

带有 epsilon 生成的 CFSM

CFSM with epsilon productions

如果我们查看计算器语言的 CFSM,我们会发现状态 0 是唯一需要更改的状态,以便允许空语句列表。项目

If we look at the CFSM for the calculator language, we discover that State 0 is the only state that needs to be changed in order to allow empty statement lists. The item

stmt_list → • stmt

stmt_list → • stmt

变成

becomes

stmt_list → • ε

stmt_list → • ε

相当于

which is equivalent to

stmt_listε

stmt_listε

或者简单地

or simply

stmt_list → •

stmt_list → •

整个州

The entire state is then

程序→ • stmt_list $$stmt_list上移位并转到 2
stmt_list → • stmt_list stmt
stmt_list → •在 $$ 减少(弹出 0 个状态,在输入时推送stmt_list
stmt → • id := expron id shift 并转到 3
stmt → • 读取 id读取移位并转到 1
stmt → • 写入expr写入移位并转到 4

前瞻项目

The look-ahead for item

stmt_list → •

stmt_list → •

FOLLOW ( stmt_list ),即结束标记$$。由于$$不会出现在此状态下任何其他项目的前瞻中,因此我们的语法仍为 SLR(1)。值得注意的是,epsilon 产生式通常会阻止语法成为 LR(0):如果此类产生式与点位于终结符之前的项目共享状态,我们将无法在不前瞻的情况下判断是否“识别” ε 。■

is FOLLOW(stmt_list), which is the end-marker, $$. Since $$ does not appear in the look-aheads for any other item in this state, our grammar is still SLR(1). It is worth noting that epsilon productions commonly prevent a grammar from being LR(0): if such a production shares a state with an item in which the dot precedes a terminal, we won't be able to tell whether to “recognize” ε without peeking ahead. ■

02-01-9780124104099检查你的理解

Check Your Understanding

37. 右句子形式的句柄是什么?

37. What is the handle of a right sentential form?

38. 解释特征有限状态机在LR解析中的意义。

38. Explain the significance of the characteristic finite-state machine in LR parsing.

39.  LR 项目中的点 (•) 有何意义?

39. What is the significance of the dot (•) in an LR item?

40.  LR 状态的基础闭合有何区别?

40. What distinguishes the basis from the closure of an LR state?

41. 什么是移位-归约冲突?在各种 LR 系列解析器中如何解决该问题?

41. What is a shift-reduce conflict? How is it resolved in the various kinds of LR-family parsers?

42. 概述自下而上的解析器驱动程序执行的步骤。

42. Outline the steps performed by the driver of a bottom-up parser.

43.  yacc/bison生成哪种类型的解析器?通过 ANTLR 生成?

43. What kind of parser is produced by yacc/bison? By ANTLR?

44. 为什么在LR(0)语法中从来没有任何epsilon产生式?

44. Why are there never any epsilon productions in an LR(0) grammar?

2.3.5 语法错误

2.3.5 Syntax Errors

例 2.42

Example 2.42

C 中的语法错误

A syntax error in C

假设我们正在解析一个 C 程序,并在需要语句的上下文中看到以下代码片段:

Suppose we are parsing a C program and see the following code fragment in a context where a statement is expected:

A=B:C+D;

A = B : C + D;

当扫描器中出现冒号时,我们将在B之后立即检测到语法错误。此时,最简单的做法就是打印一条错误消息并停止。然而,这种幼稚的方法通常是不可接受的:这意味着编译器的每次运行都不会发现一个以上的语法错误。由于大多数程序(至少在开始时)都包含大量此类错误,因此我们现在确实需要尽可能多地找到错误(我们还想继续寻找语义错误)。为此,我们必须修改解析器和/或输入流的状态,以使即将到来的标记是可以接受的。我们可能希望关闭代码生成,禁用编译器的后端:由于输入不是有效程序,因此代码将无用,并且花时间创建它也没有意义。■

We will detect a syntax error immediately after the B, when the colon appears from the scanner. At this point the simplest thing to do is just to print an error message and halt. This naive approach is generally not acceptable, however: it would mean that every run of the compiler reveals no more than one syntax error. Since most programs, at least at first, contain numerous such errors, we really need to find as many as possible now (we'd also like to continue looking for semantic errors). To do so, we must modify the state of the parser and/or the input stream so that the upcoming token(s) are acceptable. We shall probably want to turn off code generation, disabling the back end of the compiler: since the input is not a valid program, the code will not be of use, and there's no point in spending time creating it. ■

一般来说,语法错误恢复这一术语适用于任何允许编译器在遇到语法错误时继续在程序后面查找其他错误的技术。高质量的语法错误恢复对于任何生产质量的编译器都是必不可少的。恢复技术越好,编译器就越有可能正确识别其他错误(尤其是附近的错误),并且越不可能在程序后面混淆并报告虚假的级联错误。

In general, the term syntax error recovery is applied to any technique that allows the compiler, in the face of a syntax error, to continue looking for other errors later in the program. High-quality syntax error recovery is essential in any production-quality compiler. The better the recovery technique, the more likely the compiler will be to recognize additional errors (especially nearby errors) correctly, and the less likely it will be to become confused and announce spurious cascading errors later in the program.

02-02-9780124104099 更深入地

IN MORE DEPTH

在配套网站上,我们探讨了几种可能的语法错误恢复方法。在恐慌模式下,编译器编写者定义了一小组“安全符号”,用于界定输入中的干净点。分号通常用于结束语句,在许多语言中都是不错的选择。当发生错误时,编译器会删除输入标记,直到找到安全符号,然后“退出解析器”(例如,从递归下降子例程返回),直到找到可能出现该符号的上下文。短语级恢复通过在不同的语法产生式中使用不同的“安全”符号集(在表达式中时使用右括号;在声明中时使用分号)来改进这种技术。上下文特定的前瞻通过区分给定产生式可能出现在不同上下文中来获得额外的改进语法树。为了妥善应对某些常见的编程错误,编译器编写者可能会在语法中添加错误产生式,以捕获不正确但经常被错误编写的语言特定习语。

On the companion site we explore several possible approaches to syntax error recovery. In panic mode, the compiler writer defines a small set of “safe symbols” that delimit clean points in the input. Semicolons, which typically end a statement, are a good choice in many languages. When an error occurs, the compiler deletes input tokens until it finds a safe symbol, and then “backs the parser out” (e.g., returns from recursive descent subroutines) until it finds a context in which that symbol might appear. Phrase-level recovery improves on this technique by employing different sets of “safe” symbols in different productions of the grammar (right parentheses when in an expression; semicolons when in a declaration). Context-specific look-ahead obtains additional improvements by differentiating among the various contexts in which a given production might appear in a syntax tree. To respond gracefully to certain common programming errors, the compiler writer may augment the grammar with error productions that capture language-specific idioms that are incorrect but are often written by mistake.

Niklaus Wirth 于 1976 年发表了递归下降解析器的短语级和上下文特定恢复的优雅实现 [ Wir76,第 5.9 节]。如果编译器所用的语言支持异常(将在第 9.4 节中进一步讨论),则异常提供了一种更简单的替代方案。对于表驱动的自上而下的解析器,Fischer、Milton 和 Quiring 于 1980 年发表了一种算法,该算法自动实现了明确定义的局部最小成本语法修复概念。局部最小成本修复也可以在自下而上的解析器中实现,但难度要大得多。大多数自下而上的解析器依赖于更直接的短语级恢复;典型示例可在yacc/bison中找到。

Niklaus Wirth published an elegant implementation of phrase-level and context-specific recovery for recursive descent parsers in 1976 [Wir76, Sec. 5.9]. Exceptions (to be discussed further in Section 9.4) provide a simpler alternative if supported by the language in which the compiler is written. For table-driven top-down parsers, Fischer, Milton, and Quiring published an algorithm in 1980 that automatically implements a well-defined notion of locally least-cost syntax repair. Locally least-cost repair is also possible in bottom-up parsers, but it is significantly more difficult. Most bottom-up parsers rely on more straightforward phrase-level recovery; a typical example can be found in yacc/bison.

2.4 理论基础

2.4 Theoretical Foundations

我们对扫描器、解析器、正则表达式和上下文无关语法的相对角色和计算能力的理解基于自动机理论的形式化。在自动机理论中,形式语言是从有限字母表中抽取的一组符号串。形式语言可以通过生成语言的一组规则(如正则表达式或上下文无关语法)来指定,也可以通过接受识别)该语言的形式机器来指定。形式机器将符号串作为输入,输出“是”或“否”。如果机器对某种语言中的所有字符串(且只对其中某些字符串)都说“是”,则称机器接受该语言。或者,语言也可以定义为某台机器说“是”的字符串集合。

Our understanding of the relative roles and computational power of scanners, parsers, regular expressions, and context-free grammars is based on the formalisms of automata theory. In automata theory, a formal language is a set of strings of symbols drawn from a finite alphabet. A formal language can be specified either by a set of rules (such as regular expressions or a context-free grammar) that generates the language, or by a formal machine that accepts (recognizes) the language. A formal machine takes strings of symbols as input and outputs either “yes” or “no.” A machine is said to accept a language if it says “yes” to all and only those strings that are in the language. Alternatively, a language can be defined as the set of strings for which a particular machine says “yes.”

形式语言可以分为一系列逐渐增大的类别,称为乔姆斯基层次结构。14大多数类别可以用两种方式来表征:可通过可用于生成字符串集的规则类型来表征,或可通过能够识别该语言的形式机器类型来表征。如我们所见,正则语言是使用连接、交替和 Kleene 闭包来定义的,并可由扫描器识别。上下文无关语言是正则语言的真正超集。它们可通过连接、交替和递归(包含 Kleene 闭包)来定义,并可由解析器识别。扫描器是有限自动机(一种形式机器)的具体实现。解析器是下推自动机的具体实现。正如上下文无关语法为正则表达式添加递归一样,下推自动机为有限自动机的内存添加了堆栈。乔姆斯基层次结构中还有其他级别,但它们与编译器构造的直接适用性较差,因此本文不再赘述。

Formal languages can be grouped into a series of successively larger classes known as the Chomsky hierarchy.14 Most of the classes can be characterized in two ways: by the types of rules that can be used to generate the set of strings, or by the type of formal machine that is capable of recognizing the language. As we have seen, regular languages are defined by using concatenation, alternation, and Kleene closure, and are recognized by a scanner. Context-free languages are a proper superset of the regular languages. They are defined by using concatenation, alternation, and recursion (which subsumes Kleene closure), and are recognized by a parser. A scanner is a concrete realization of a finite automaton, a type of formal machine. A parser is a concrete realization of a push-down automaton. Just as context-free grammars add recursion to regular expressions, push-down automata add a stack to the memory of a finite automaton. There are additional levels in the Chomsky hierarchy, but they are less directly applicable to compiler construction, and are not covered here.

可以建设性地证明,正则表达式和有限自动机是等价的:可以构造一个有限自动机,它接受由给定正则表达式定义的语言,反之亦然。类似地,可以构造一个下推自动机,它接受由给定上下文无关语法定义的语言,反之亦然。语法到自动机的构造实际上是由扫描器和解析器生成器(例如lexyacc )执行的。当然,真正的扫描器不会只接受一个标记;它会在循环中被调用,以便不断重复接受标记。如边栏 2.4 中所述,通过让扫描器接受语言中所有标记的交替(具有不同的最终状态),并让它继续使用字符,直到无法再构造标记,来实现这个细节。

It can be proven, constructively, that regular expressions and finite automata are equivalent: one can construct a finite automaton that accepts the language defined by a given regular expression, and vice versa. Similarly, it is possible to construct a push-down automaton that accepts the language defined by a given context-free grammar, and vice versa. The grammar-to-automaton constructions are in fact performed by scanner and parser generators such as lex and yacc. Of course, a real scanner does not accept just one token; it is called in a loop so that it keeps accepting tokens repeatedly. As noted in Sidebar 2.4, this detail is accommodated by having the scanner accept the alternation of all the tokens in the language (with distinguished final states), and by having it continue to consume characters until no longer token can be constructed.

02-02-9780124104099 更深入地

IN MORE DEPTH

在配套网站上,我们更详细地讨论了有限自动机和下推自动机。我们给出了一个将 DFA 转换为等效正则表达式的算法。结合第2.2.1 节中的构造,该算法证明了正则表达式和有限自动机的等价性。我们还考虑了各种线性时间解析算法可以和不能解析的语法和语言集。

On the companion site we consider finite and pushdown automata in more detail. We give an algorithm to convert a DFA into an equivalent regular expression. Combined with the constructions in Section 2.2.1, this algorithm demonstrates the equivalence of regular expressions and finite automata. We also consider the sets of grammars and languages that can and cannot be parsed by the various linear-time parsing algorithms.

2.5 总结与结束语

2.5 Summary and Concluding Remarks

在本章中,我们介绍了正则表达式和上下文无关文法的形式化,以及实际编译器中扫描和解析的基础算法。我们还提到了语法错误恢复,并简要概述了自动机理论的相关部分。正则表达式和上下文无关文法是语言生成器:它们指定如何构造有效的字符串或标记。扫描器和解析器是语言识别器:它们指示给定的字符串是否有效。扫描器的主要工作是通过将字符分组为标记以及删除注释和空格来减少解析器必须处理的信息量。扫描器和解析器生成器会自动将正则表达式和上下文无关文法转换为扫描器和解析器。

In this chapter we have introduced the formalisms of regular expressions and context-free grammars, and the algorithms that underlie scanning and parsing in practical compilers. We also mentioned syntax error recovery, and presented a quick overview of relevant parts of automata theory. Regular expressions and context-free grammars are language generators: they specify how to construct valid strings of characters or tokens. Scanners and parsers are language recognizers: they indicate whether a given string is valid. The principal job of the scanner is to reduce the quantity of information that must be processed by the parser, by grouping characters together into tokens, and by removing comments and white space. Scanner and parser generators automatically translate regular expressions and context-free grammars into scanners and parsers.

编程语言的实用解析器(以线性时间运行的解析器)主要分为两类:自上而下(也称为 LL 或预测)和自下而上(也称为 LR 或移位归约)。自上而下的解析器从根开始构建解析树,然后从左到右进行深度优先遍历。自下而上的解析器从叶子开始构建解析树,同样从左到右进行,并在识别出内部节点的子节点时将部分树合并在一起。自上而下的解析器的堆栈包含对未来将看到的内容的预测;自下而上的解析器的堆栈包含过去看到的内容的记录。

Practical parsers for programming languages (parsers that run in linear time) fall into two principal groups: top-down (also called LL or predictive) and bottom-up (also called LR or shift-reduce). A top-down parser constructs a parse tree starting from the root and proceeding in a left-to-right depth-first traversal. A bottom-up parser constructs a parse tree starting from the leaves, again working left-to-right, and combining partial trees together when it recognizes the children of an internal node. The stack of a top-down parser contains a prediction of what will be seen in the future; the stack of a bottom-up parser contains a record of what has been seen in the past.

自上而下的解析器往往比较简单,无论是在解析有效字符串方面,还是在从无效字符串的错误中恢复方面。自下而上的解析器功能更强大,在某些情况下,它们更适合于更直观的结构化语法,但它们无法在右侧的任意位置嵌入动作例程(我们将在 C-4.5.1 节中更详细地讨论这一点)。这两种解析器都在实际编译器中使用,但自下而上的解析器更为常见。自上而下的解析器在代码和数据大小方面往往较小,但现代机器为这两种解析器都提供了充足的内存。

Top-down parsers tend to be simple, both in the parsing of valid strings and in the recovery from errors in invalid strings. Bottom-up parsers are more powerful, and in some cases lend themselves to more intuitively structured grammars, though they suffer from the inability to embed action routines at arbitrary points in a right-hand side (we discuss this point in more detail in Section C-4.5.1). Both varieties of parser are used in real compilers, though bottom-up parsers are more common. Top-down parsers tend to be smaller in terms of code and data size, but modern machines provide ample memory for either.

如果没有自动工具,可以手动构建扫描器和解析器。手工构建的扫描器非常简单,比较常见。手工构建的解析器通常仅限于自上而下的递归下降,最常用于相对简单的语言。自动生成扫描器和解析器具有可靠性更高、开发时间更短、易于修改和增强的优点。

Both scanners and parsers can be built by hand if an automatic tool is not available. Handbuilt scanners are simple enough to be relatively common. Hand-built parsers are generally limited to top-down recursive descent, and are most commonly used for comparatively simple languages. Automatic generation of the scanner and parser has the advantage of increased reliability, reduced development time, and easy modification and enhancement.

语言设计的各种特性会对语法分析的复杂性产生重大影响。在许多情况下,使编译器难以扫描或解析的特性也会使人类难以编写正确、可维护的代码。例子包括 Fortran 的词汇结构和Pascal 等语言的if…then…else语句。语言设计、实现和使用之间的这种相互作用将是本书其余部分反复出现的主题。

Various features of language design can have a major impact on the complexity of syntax analysis. In many cases, features that make it difficult for a compiler to scan or parse also make it difficult for a human being to write correct, maintainable code. Examples include the lexical structure of Fortran and the if… then … else statement of languages like Pascal. This interplay among language design, implementation, and use will be a recurring theme throughout the remainder of the book.

2.6 练习

2.6 Exercises

2.1 编写正则表达式来捕获以下内容。

2.1 Write regular expressions to capture the following.

(a)  C 中的字符串。这些字符串由双引号 (“) 分隔,并且不能包含换行符。当且仅当这些字符被前面的反斜杠“转义”时,它们才可以包含双引号或反斜杠字符。您可能会发现,引入简写符号来表示属于指定小集合的任何字符会很有帮助。

(a) Strings in C. These are delimited by double quotes (“), and may not contain newline characters. They may contain double-quote or backslash characters if and only if those characters are “escaped” by a preceding backslash. You may find it helpful to introduce shorthand notation to represent any character that is not a member of a small specified set.

(b) Pascal 中的注释。这些注释由 (* 和 *) 或 { 和 } 分隔。不允许嵌套。

(b) Comments in Pascal. These are delimited by (* and *) or by { and }. They are not permitted to nest.

(c)  C 中的数字常量。这些是八进制、十进制或十六进制整数,或十进制或十六进制浮点值。八进制整数以0开头,可能只包含数字0–7。十六进制整数以0x0X开头,可能包含数字0–9a/A–f/F。十进制浮点值具有小数部分(以点开头)或指数(以E或 e 开头)。与十进制整数不同,它可以以0开头。十六进制浮点值具有可选的小数部分和必需的指数(以Pp开头)。无论是十进制还是十六进制,都可以有数字在点的左侧、点的右侧或两者,指数本身以十进制表示,带有可选的前导+符号。整数可以以可选的Uu(表示“无符号”)和/或Ll(表示“长”)或LLll (表示“长长”)结尾。浮点值可以以可选的Ff(表示“浮点” - 单精度)或Ll(表示“长” - 双精度)结尾。

(c) Numeric constants in C. These are octal, decimal, or hexadecimal integers, or decimal or hexadecimal floating-point values. An octal integer begins with 0, and may contain only the digits 0–7. A hexadecimal integer begins with 0x or 0X, and may contain the digits 0–9 and a/A–f/F. A decimal floating-point value has a fractional portion (beginning with a dot) or an exponent (beginning with E or e). Unlike a decimal integer, it is allowed to start with 0. A hexadecimal floating-point value has an optional fractional portion and a mandatory exponent (beginning with P or p). In either decimal or hexadecimal, there may be digits to the left of the dot, the right of the dot, or both, and the exponent itself is given in decimal, with an optional leading + or sign. An integer may end with an optional U or u (indicating “unsigned”), and/or L or l (indicating “long”) or LL or ll (indicating “long long”). A floating-point value may end with an optional F or f (indicating “float”—single precision) or L or l (indicating “long”—double precision).

(d)  Ada 中的浮点常量。这些与示例 2.3中的实数定义相符,不同之处在于 (1) 小数点两边都需要一个数字,(2) 数字之间允许使用下划线,以及 (3) 可以通过用磅号将数字的非指数部分括起来并在前面加上十进制基数来指定替代数字基数(例如,16#6.a7#e+2)。在后一种情况下,字母a .. f(大写和小写)可以用作数字。在不适当的数字(例如十进制)中使用这些字母是一种错误,但扫描仪无需捕获。

(d) Floating-point constants in Ada. These match the definition of real in Example 2.3, except that (1) a digit is required on both sides of the decimal point, (2) an underscore is permitted between digits, and (3) an alternative numeric base may be specified by surrounding the nonexponent part of the number with pound signs, preceded by a base in decimal (e.g., 16#6.a7#e+2). In this latter case, the letters a .. f (both upper- and lowercase) are permitted as digits. Use of these letters in an inappropriate (e.g., decimal) number is an error, but need not be caught by the scanner.

(e)  Scheme 中的不精确常数。Scheme 允许实数明确地不精确(不精确)。如果程序员想要使用相同数量的字符来表达所有常数,可以使用尖号 (#) 代替任何未知值的低位有效数字。无指数的十进制常数由一个或多个数字后跟零或更多尖号组成。可选的小数点可以放在开头、结尾或中间的任何地方。(需要说明的是,Scheme 中的数字实际上比这复杂得多。出于本练习的目的,请忽略您可能知道的有关符号、指数、基数、精确度和长度说明符以及复数或有理值的任何内容。)

(e) Inexact constants in Scheme. Scheme allows real numbers to be explicitly inexact (imprecise). A programmer who wants to express all constants using the same number of characters can use sharp signs (#) in place of any lower-significance digits whose values are not known. A base-10 constant without exponent consists of one or more digits followed by zero of more sharp signs. An optional decimal point can be placed at the beginning, the end, or anywhere in-between. (For the record, numbers in Scheme are actually a good bit more complicated than this. For the purposes of this exercise, please ignore anything you may know about sign, exponent, radix, exactness and length specifiers, and complex or rational values.)

(f) 美式财务数量。它们以美元符号 ($ 为前导)、可选的星号字符串(*——用于支票以防欺诈)、十进制数字字符串以及可选的小数部分(由一个小数点 (.) 和两个小数组成)。小数点左边的数字字符串可以由一个零 ( 0 ) 组成。否则,它不能以零开头。如果小数点左边的数字超过三位,则三组(从右边开始数)必须用逗号 (,) 分隔。例如:$**2,345.67。(可以随意使用“产品”来定义缩写,只要语言保持规则即可。)

(f) Financial quantities in American notation. These have a leading dollar sign ($), an optional string of asterisks (*—used on checks to discourage fraud), a string of decimal digits, and an optional fractional part consisting of a decimal point (.) and two decimal digits. The string of digits to the left of the decimal point may consist of a single zero (0). Otherwise it must not start with a zero. If there are more than three digits to the left of the decimal point, groups of three (counting from the right) must be separated by commas (,). Example: $**2,345.67. (Feel free to use “productions” to define abbreviations, so long as the language remains regular.)

2.2 以“圆圈和箭头”图的形式展示练习 2.1 的有限自动机。

2.2 Show (as “circles-and-arrows” diagrams) the finite automata for Exercise 2.1.

2.3 构建一个正则表达式,捕获除fileforfrom之外的所有非空字母序列。为了方便记号,可以假设存在一个not运算符,该运算符以一组字母为参数,并匹配任何其他字母。评价为大型编程语言的关键字之外的所有字母序列构建正则表达式的实用性。

2.3 Build a regular expression that captures all nonempty sequences of letters other than file, for, and from. For notational convenience, you may assume the existence of a not operator that takes a set of letters as argument and matches any other letter. Comment on the practicality of constructing a regular expression for all sequences of letters other than the keywords of a large programming language.

2.4 

2.4 

(一个) 展示将图 2.7的构造应用于正则表达式字母(字母|数字)*所得的 NFA 。

(a) Show the NFA that results from applying the construction of Figure 2.7 to the regular expression letter ( letter | digit )*.

(b)应用 例 2.14所示的转换来创建等效 DFA。

(b) Apply the transformation illustrated by Example 2.14 to create an equivalent DFA.

(c)应用 例 2.15所示的变换来最小化 DFA。

(c) Apply the transformation illustrated by Example 2.15 to minimize the DFA.

2.5从 示例 2.3中的整数小数的正则表达式开始,构造一个等效 NFA、子集集 DFA 和最小等效 DFA。确保将两种不同类型的 token 的最终状态分开(参见边栏 2.4)。如果您通过修改示例 2.13 至 2.15 中的机器来完成此练习,您可能会发现它容易

2.5 Starting with the regular expressions for integer and decimal in Example 2.3, construct an equivalent NFA, the set-of-subsets DFA, and the minimal equivalent DFA. Be sure to keep separate the final states for the two different kinds of token (see Sidebar 2.4). You may find the exercise easier if you undertake it by modifying the machines in Examples 2.13 through 2.15.

2.6 为计算器语言构建一个临时扫描器。作为输出,让它按顺序打印输入标记的列表。为简单起见,如果出现词汇错误,请随意停止。

2.6 Build an ad hoc scanner for the calculator language. As output, have it print a list, in order, of the input tokens. For simplicity, feel free to simply halt in the event of a lexical error.

2.7 用你最喜欢的脚本语言编写一个程序,从计算器语言程序中删除注释(示例 2.9)。

2.7 Write a program in your favorite scripting language to remove comments from programs in the calculator language (Example 2.9).

2.8 构建一个嵌套case语句的有限自动机,将输入的所有字母转换为小写,但 Pascal 风格的注释和字符串除外。Pascal 注释以 { 和 } 或 (* 和 *) 分隔。注释不能嵌套。Pascal 字符串以单引号 (' … ') 分隔。引号字符可以通过将其加倍来放置在字符串中 ( 'Madam, I' 'm Adam.' )。如果将用标准 Pascal (忽略大小写) 编写的程序输入到将大小写字母视为不同的编译器,则这种从大到小的映射非常有用。

2.8 Build a nested-case-statements finite automaton that converts all letters in its input to lower case, except within Pascal-style comments and strings. A Pascal comment is delimited by { and }, or by (* and *). Comments do not nest. A Pascal string is delimited by single quotes (' … '). A quote character can be placed in a string by doubling it ('Madam, I' 'm Adam.'). This upper-to-lower mapping can be useful if feeding a program written in standard Pascal (which ignores case) to a compiler that considers upper- and lowercase letters to be distinct.

2.9 

2.9 

(a)用英语描述正则表达式 a*(ba* ba*)*定义的语言。你的描述应该是高级特征——即使我们对同一种语言使用不同的正则表达式,这种描述仍然有意义。

(a) Describe in English the language defined by the regular expression a*(b a* b a*)*. Your description should be a high-level characterization—one that would still make sense if we were using a different regular expression for the same language.

(b) 编写一个能够生成相同语言的明确的上下文无关文法。

(b) Write an unambiguous context-free grammar that generates the same language.

(c) 使用部分(b)中的语法,对字符串baabaaabb进行规范(最右边)推导。

(c) Using your grammar from part (b), give a canonical (right-most) derivation of the string b a a b a a a b b.

2.10 给出一个捕获指数运算符右结合性的语法示例(例如 Fortran 中的 **)。

2.10 Give an example of a grammar that captures right associativity for an exponentiation operator (e.g., ** in Fortran).

2.11 证明下列文法是LL(1):

2.11 Prove that the following grammar is LL(1):

 声明→ ID声明

 decl → ID decl_tail

 decl_tail →,decl

 decl_tail →, decl

  → :身份证;

  → : ID ;

(最后一个ID是类型名称。)

(The final ID is meant to be a type name.)

2.12 考虑以下文法:GS $$ SAM MS | ε A → a E | b AA E → a B | b A | ε B → b E | a BB

 

 

 

 

 

 

2.12 Consider the following grammar:

 GS $$

 SA M

 MS | ε

 A → a E | b A A

 E → a B | b A | ε

 B → b E | a B B

(一个) 用英语描述语法生成的语言。

(a) Describe in English the language that the grammar generates.

(b) 显示字符串abaa的解析树。

(b) Show a parse tree for the string a b a a.

(c) 该语法是 LL(1) 吗?如果是,则显示解析表;如果不是,则确定预测冲突。

(c) Is the grammar LL(1)? If so, show the parse table; if not, identify a prediction conflict.

2.13 考虑以下语法:stmtassignmentsubr_call assignment → id := expr subr_call → id ( arg_list ) exprprimary expr_tail expr_tailop exprε primary → id subr_call → ( expr ) op → + | − | * | / arg_listexpr args_tail args_tail →, arg_listε

 

  

 

 

 

 

  

 

  

  

 

 

 

  

2.13 Consider the following grammar:

 stmtassignment

  subr_call

 assignment → id := expr

 subr_call → id ( arg_list)

 exprprimary expr_tail

 expr_tailop expr

  ε

 primary → id

  subr_call

  → ( expr )

 op → + | − | * | /

 arg_listexpr args_tail

 args_tail →, arg_list

  ε

(a) 为输入字符串foo(a, b)构建一棵解析树。

(a) Construct a parse tree for the input string foo(a, b).

(b) 给出该相同字符串的规范(最右边)推导。

(b) Give a canonical (right-most) derivation of this same string.

(c) 证明该文法不是LL(1)文法。

(c) Prove that the grammar is not LL(1).

(d) 修改文法,使其成为LL(1)。

(d) Modify the grammar so that it isLL(1).

2.14 考虑由所有适当平衡的括号和方括号字符串组成的语言。

2.14 Consider the language consisting of all strings of properly balanced parentheses and brackets.

(a) 给出该语言的 LL(1) 和 SLR(1) 语法。

(a) Give LL(1) and SLR(1) grammars for this language.

(b) 给出相应的LL(1)和SLR(1)解析表。

(b) Give the corresponding LL(1) and SLR(1) parsing tables.

(c)对于每个语法,显示 ([]([]))[](())的解析树。

(c) For each grammar, show the parse tree for ([]([]))[](()).

(d) 跟踪分析器在构建这些树时的操作。

(d) Give a trace of the actions of the parsers in constructing these trees.

2.15 考虑以下上下文无关语法。GGBGNε B → ( E ) EE ( E ) ε N → ( L ] LLEL ( ε

 

  

  

 

 

  

 

 

  

  

2.15 Consider the following context-free grammar.

 GG B

  G N

  ε

 B → ( E )

 EE ( E )

  ε

 N → ( L ]

 LL E

  L (

  ε

(一个) 用英语描述该语法生成​​的语言。(提示:B代表“平衡”;N代表“不平衡”。)(您的描述应该是该语言的高级特征——与所选的特定语法无关。)

(a) Describe, in English, the language generated by this grammar. (Hint: B stands for “balanced”; N stands for “nonbalanced”.) (Your description should be a high-level characterization of the language—one that is independent of the particular grammar chosen.)

(b) 给出字符串 ((]() 的一棵分析树。

(b) Give a parse tree for the string ((]().

(c) 给出该相同字符串的规范(最右边)推导。

(c) Give a canonical (right-most) derivation of this same string.

(d) 我们的语法中的FIRST( E ) 是什么?FOLLOW( E ) 是什么?(回想一下,FIRST 和 FOLLOW 集合是为任意 CFG 中的符号定义的,与解析算法无关。)

(d) What is FIRST(E) in our grammar? What is FOLLOW(E)? (Recall that FIRST and FOLLOW sets are defined for symbols in an arbitrary CFG, regardless of parsing algorithm.)

(e) 鉴于其使用左递归,我们的语法显然不是 LL(1)。该语言有 LL(1) 语法吗?解释一下。

(e) Given its use of left recursion, our grammar is clearly not LL(1). Does this language have an LL(1) grammar? Explain.

2.16给出一个文法,该文法可以涵盖 C 语言中算术表达式的所有优先级,如图 6.1所示。(提示:这个练习有点乏味。你可能需要用文本编辑器而不是铅笔来完成它。)

2.16 Give a grammar that captures all levels of precedence for arithmetic expressions in C, as shown in Figure 6.1. (Hint: This exercise is somewhat tedious. You'll probably want to attack it with a text editor rather than a pencil.)

2.17扩展 图 2.25中的语法,使其包含if语句和while循环,具体方法如下:abs := n if n < 0 then abs := 0 - abs fi sum := 0 read count while count > 0 do read n sum := sum + n count := count - 1 od write sum语法应支持条件中的六种标准比较运算,以任意表达式作为操作数。它还应允许ifwhile语句主体中有任意数量的语句

 

 

 

 

 

  

  

  

 

 

2.17 Extend the grammar of Figure 2.25 to include if statements and while loops, along the lines suggested by the following examples:

 abs := n

 if n < 0 then abs := 0 - abs fi

 sum := 0

 read count

 while count > 0 do

  read n

  sum := sum + n

  count := count - 1

 od

 write sum

Your grammar should support the six standard comparison operations in conditions, with arbitrary expressions as operands. It should also allow an arbitrary number of statements in the body of an if or while statement.

2.18 考虑以下简化 Lisp 子集的 LL(1) 语法:PE $$ E → atom → ' E → ( E Es ) EsE Es

 

 

  

  

 

  

2.18 Consider the following LL(1) grammar for a simplified subset of Lisp:

 PE $$

 E → atom

  → ’ E

  → ( E Es )

 EsE Es

  

(a) 什么是 FIRST( Es )?FOLLOW( E )?PREDICT( Esε )?

(a) What is FIRST(Es)? FOLLOW(E)? PREDICT(Esε)?

(b) 给出字符串(cdr '(abc)) $$ 的一棵分析树。

(b) Give a parse tree for the string (cdr '(a b c)) $$.

(c)显示 (cdr '(abc)) $$的最左导数。

(c) Show the left-most derivation of (cdr '(a b c)) $$.

(d)以 图 2.21的风格展示对同一输入进行表驱动自上而下解析的轨迹。

(d) Show a trace, in the style of Figure 2.21, of a table-driven top-down parse of this same input.

(e) 现在考虑在相同输入上运行的递归下降解析器。在匹配引号标记 (') 的位置,哪些递归下降例程将处于活动状态(即,哪些例程将在解析器的运行时堆栈上有一个框架)?

(e) Now consider a recursive descent parser running on the same input. At the point where the quote token (') is matched, which recursive descent routines will be active (i.e., what routines will have a frame on the parser's run-time stack)?

2.19 为由所有格式正确的正则表达式组成的语言编写自上而下和自下而上的文法。将所有运算符安排为左结合的。赋予 Kleene 闭包最高优先级,赋予交替最低优先级。

2.19 Write top-down and bottom-up grammars for the language consisting of all well-formed regular expressions. Arrange for all operators to be left-associative. Give Kleene closure the highest precedence and alternation the lowest precedence.

2.20 假设示例 2.8中的表达式语法与扫描器结合使用,该扫描器不会输入中删除注释,而是将其作为标记返回。需要如何修改语法才能允许注释出现在输入中的任意位置?

2.20 Suppose that the expression grammar in Example 2.8 were to be used in conjunction with a scanner that did not remove comments from the input, but rather returned them as tokens. How would the grammar need to be modified to allow comments to appear at arbitrary places in the input?

2.21 为计算器语言构建一个完整的递归下降解析器。作为输出,让它打印匹配和预测的轨迹。

2.21 Build a complete recursive descent parser for the calculator language. As output, have it print a trace of its matches and predictions.

2.22 将你的解决方案扩展至练习 2.21,以构建一个显式的解析树。

2.22 Extend your solution to Exercise 2.21 to build an explicit parse tree.

2.23 将你的解决方案扩展至练习 2.21,以直接构建抽象语法树,而无需先构建解析树。

2.23 Extend your solution to Exercise 2.21 to build an abstract syntax tree directly, without constructing a parse tree first.

2.24 Pascal 的 悬空else问题并没有出现在其前身 Algol 60 中。为了避免对else匹配哪个then产生歧义,Algol 60 禁止在then子句内直接使用 if语句。Pascal 片段if C1 then if C2 then S1 else S2必须写成if C1 then begin if C2 then S1 end else S2if C1 then begin if C2 then S1 else S2 end

 



 



 

在 Algol 60 中。说明如何编写强制执行此规则的条件语句语法。(提示:您需要在语法中区分条件语句和非条件语句;有些上下文会接受其中任何一种,有些只接受后者。)

2.24 The dangling else problem of Pascal was not shared by its predecessor Algol 60. To avoid ambiguity regarding which then is matched by an else, Algol 60 prohibited if statements immediately inside a then clause. The Pascal fragment

 if C1 then if C2 then S1 else S2

had to be written as either

 if C1 then begin if C2 then S1 end else S2

or

 if C1 then begin if C2 then S1 else S2 end

in Algol 60. Show how to write a grammar for conditional statements that enforces this rule. (Hint: You will want to distinguish in your grammar between conditional statements and nonconditional statements; some contexts will accept either, some only the latter.)

2.25 充实算法的细节,以消除任意上下文无关文法中的左递归和公共前缀。

2.25 Flesh out the details of an algorithm to eliminate left recursion and common prefixes in an arbitrary context-free grammar.

2.26 在某些语言中,赋值可以出现在任何需要表达式的上下文中:表达式的值是赋值的右侧,作为副作用,它被放置在左侧。考虑这种语言的以下语法片段。解释为什么它不是 LL(1),并讨论可以做些什么来使其成为 LL(1)。expr → id := expr → term term_tail term_tail → + term term_tail | ε term → factor factor_tail factor_tail → * factor factor_tail | ε factor → ( expr ) | id

 

  

 

 

 

 

2.26 In some languages an assignment can appear in any context in which an expression is expected: the value of the expression is the right-hand side of the assignment, which is placed into the left-hand side as a side effect. Consider the following grammar fragment for such a language. Explain why it is not LL(1), and discuss what might be done to make it so.

 expr → id := expr

  → term term_tail

 term_tail → + term term_tail | ε

 term → factor factor_tail

 factor_tail → * factor factor_tail | ε

 factor → ( expr ) | id

2.27为 示例 2.20中的id_list文法构建 CFSM ,并验证它可以自下而上地用零个前瞻标记进行解析。

2.27 Construct the CFSM for the id_list grammar in Example 2.20 and verify that it can be parsed bottom-up with zero tokens of look-ahead.

2.28修改 练习 2.27中的文法,使其允许id_list为空。该文法是否仍为 LR(0)?

2.28 Modify the grammar in Exercise 2.27 to allow an id_list to be empty. Is the grammar still LR(0)?

2.29使用 图 2.15的语法重复示例 2.36

2.29 Repeat Example 2.36 using the grammar of Figure 2.15.

2.30 考虑以下声明列表的文法:decl_listdecl_list decl ; | decl ; decl → id : type type → int | real | char → array const .. const of type → record decl_list end构造该文法的 CFSM。使用它来追踪以下输入程序的解析(如图2.30所示): foo : record a : char; b : array 1 .. 2 of real; end;

 

 

 

  

  



 

  

  

 

2.30 Consider the following grammar for a declaration list:

 decl_listdecl_list decl ; | decl ;

 decl → id : type

 type → int | real | char

  → array const .. const of type

  → record decl_list end

Construct the CFSM for this grammar. Use it to trace out a parse (as in Figure 2.30) for the following input program:

 foo : record

  a : char;

  b : array 1 .. 2 of real;

 end;

02-02-97801241040992.31–2.37  更深入。

2.31–2.37  In More Depth.

2.7 探索

2.7 Explorations

2.38 有些语言(例如 C)区分标识符中的大小写字母。其他语言(例如 Ada)则不区分。您更喜欢哪种惯例?为什么?

2.38 Some languages (e.g., C) distinguish between upper- and lowercase letters in identifiers. Others (e.g., Ada) do not. Which convention do you prefer? Why?

2.39  C 及其后代中的类型转换语法引入了潜在的歧义:(x)-y是减法,还是y的一元否定,转换为类型x?了解 C、C++、Java 和 C# 如何回答这个问题。讨论如何实现答案。

2.39 The syntax for type casts in C and its descendants introduces potential ambiguity: is (x)-y a subtraction, or the unary negation of y, cast to type x? Find out how C, C++, Java, and C# answer this question. Discuss how you would implement the answer(s).

2.40 您如何看待 Haskell、Occam 和 Python 使用缩进来界定控制结构(第 2.1.1 节)?您认为这种惯例会使程序构建和维护更容易还是更困难?为什么?

2.40 What do you think of Haskell, Occam, and Python's use of indentation to delimit control constructs (Section 2.1.1)? Would you expect this convention to make program construction and maintenance easier or harder? Why?

2.41 直接跳到14.4.2 节,了解脚本语言、编辑器、搜索工具等中使用的“正则表达式”。这些真的有规律吗?它们能表达什么,而2.1.1 节介绍的符号无法表达?

2.41 Skip ahead to Section 14.4.2 and learn about the “regular expressions” used in scripting languages, editors, search tools, and so on. Are these really regular? What can they express that cannot be expressed in the notation introduced in Section 2.1.1?

2.42 使用lex/flex重建练习 2.8的自动机。

2.42 Rebuild the automaton of Exercise 2.8 using lex/flex.

2.43查找 yacc/bison手册,或查阅编译器教科书 [ ALSU07,第 4.8.1 和 4.9.2 节] 以了解运算符优先级解析。解释如何使用它来简化练习 2.16的语法。

2.43 Find a manual for yacc/bison, or consult a compiler textbook [ALSU07, Secs. 4.8.1 and 4.9.2] to learn about operator precedence parsing. Explain how it could be used to simplify the grammar of Exercise 2.16.

2.44 使用lex/flexyacc/bison为计算器语言构建一个解析器。让它输出其移位和归约的轨迹。

2.44 Use lex/flex and yacc/bison to construct a parser for the calculator language. Have it output a trace of its shifts and reductions.

2.45 使用 ANTLR 重复前面的练习。

2.45 Repeat the previous exercise using ANTLR.

02-02-97801241040992.46–2.47  更深入。

2.46–2.47  In More Depth.

2.8 书目注释

2.8 Bibliographic Notes

本章对扫描和解析的介绍很简短。在有关解析理论 [ AU72 ] 和编译器构造 [ ALSU07FCL10App97GBJ + 12CT04 ] 的文本中可以找到更详细的内容。20 世纪 60 年代早期的许多编译器都使用了递归下降解析器。Lewis 和 Stearns [ LS68 ] 以及 Rosenkrantz 和 Stearns [ RS70 ] 发表​​了早期对 LL 语法和解析的正式研究。LR 解析的原始表述归功于 Knuth [ Knu65 ]。随着 DeRemer 发现 SLR 和 LALR 算法 [ DeR71 ] ,自下而上的解析变得实用。WL Johnson 等人 [ JPAR68 ] 描述了一种早期的扫描器生成器。Unix lex工具由 Lesk [ Les75 ] 提供。Yacc源自 SC Johnson [ Joh75 ]。

Our coverage of scanning and parsing in this chapter has of necessity been brief. Considerably more detail can be found in texts on parsing theory [AU72] and compiler construction [ALSU07, FCL10, App97, GBJ+12, CT04]. Many compilers of the early 1960s employed recursive descent parsers. Lewis and Stearns [LS68] and Rosenkrantz and Stearns [RS70] published early formal studies of LL grammars and parsing. The original formulation of LR parsing is due to Knuth [Knu65]. Bottom-up parsing became practical with DeRemer's discovery of the SLR and LALR algorithms [DeR71]. W. L. Johnson et al. [JPAR68] describe an early scanner generator. The Unix lex tool is due to Lesk [Les75]. Yacc is due to S. C. Johnson [Joh75].

关于形式语言理论的更多细节可以在各种教科书中找到,包括 Hopcroft、Motwani 和 Ullman 的教科书 [ HMU07 ] 和Sipser [ Sip13 ]。Kleene [ Kle56 ] 以及 Rabin 和 Scott [ RS59 ] 证明了正则表达式与有限自动机的等价性。15有限自动机无法识别嵌套结构的证明基于Bar-Hillel、Perles 和 Shamir [ BHPS61 ] 提出的泵引理。Chomsky [ Cho56 ] 首次在自然语言环境中探索了上下文无关语法。Backus 和 Naur 独立开发了 BNF,用于 Algol 60 [ NBB + 63 ] 的句法描述。Ginsburg 和 Rice [ GR62 ] 认识到这两种符号的等价性。Chomsky [ Cho62 ] 和 Evey [ Eve63 ] 证明了上下文无关语法和下推自动机的等价性。

Further details on formal language theory can be found in a variety of textbooks, including those of Hopcroft, Motwani, and Ullman [HMU07] and Sipser [Sip13]. Kleene [Kle56] and Rabin and Scott [RS59] proved the equivalence of regular expressions and finite automata.15 The proof that finite automata are unable to recognize nested constructs is based on a theorem known as the pumping lemma, due to Bar-Hillel, Perles, and Shamir [BHPS61]. Context-free grammars were first explored by Chomsky [Cho56] in the context of natural language. Independently, Backus and Naur developed BNF for the syntactic description of Algol 60 [NBB+63]. Ginsburg and Rice [GR62] recognized the equivalence of the two notations. Chomsky [Cho62] and Evey [Eve63] demonstrated the equivalence of context-free grammars and push-down automata.

Fischer 等人的文本 [ FCL10 ] 包含对错误恢复和修复技术的出色概述,并引用了其他工作。第 C-2.3.5 节中描述的递归下降解析器的短语级恢复机制由 Wirth [ Wir76 ,第 5.9 节]提出。第 C-2.3.5 节中描述的表驱动 LL 解析器的局部最小成本恢复机制由 Fischer、Milton 和 Quiring [ FMQ80 ]提出。Dion 于 1978 年发表了局部最小成本自下而上的修复算法 [ Dio78 ]。该算法非常复杂,需要非常大的预计算表。McKenzie、Yeatman 和 De Vere 随后展示了如何在没有预计算表的情况下实现相同的修复,虽然时间成本较高,但仍在可接受的范围内 [ MYD95 ]。

Fischer et al.'s text [FCL10] contains an excellent survey of error recovery and repair techniques, with references to other work. The phrase-level recovery mechanism for recursive descent parsers described in Section C-2.3.5 is due to Wirth [Wir76, Sec. 5.9]. The locally least-cost recovery mechanism for table-driven LL parsers described in Section C-2.3.5 is due to Fischer, Milton, and Quiring [FMQ80]. Dion published a locally least-cost bottom-up repair algorithm in 1978 [Dio78]. It is quite complex, and requires very large precomputed tables. McKenzie, Yeatman, and De Vere subsequently showed how to effect the same repairs without the precomputed tables, at a higher but still acceptable cost in time [MYD95].


1斯蒂芬·克莱恩 (Stephen Kleene, 1909–1994) 是美国威斯康星大学的数学家,他对计算理论的早期发展做出了很大贡献,其中包括 C-2.4 节中的大部分材料。

1 Stephen Kleene (1909–1994), a mathematician at the University of Wisconsin, was responsible for much of the early development of the theory of computation, including much of the material in Section C-2.4.

2在许多站点上, lexyacc已经被 GNU flexbison工具所取代,它们提供了原始功能的超集。

2 At many sites, lex and yacc have been superseded by the GNU flex and bison tools, which provide a superset of the original functionality.

3有些作者使用λ来表示空字符串。有些作者使用句点 (.) 而不是并列来表示连接。有些作者使用加号 (+) 而不是竖线来表示交替。

3 Some authors use λ to represent the empty string. Some use a period (.), rather than juxtaposition, to indicate concatenation. Some use a plus sign (+), rather than a vertical bar, to indicate alternation.

4我们在此假设所有数字常量都只是“数字”。在许多编程语言中,整数和实数常量是不同的标记类型。它们的语法也可能比此处指出的更复杂,以支持这些功能需要多种长度或非十进制基数。

4 We have assumed here that all numeric constants are simply “numbers.” In many programming languages, integer and real constants are separate kinds of token. Their syntax may also be more complex than indicated here, to support such features are multiple lengths or nondecimal bases.

5为了保持一致性,我们在本书中并不总是遵循这样的惯例:大多数示例都遵循 C 程序员的常见做法,即用下划线而不是大写字母来分隔名称的“子词”。

5 For the sake of consistency we do not always obey such conventions in this book: most examples follow the common practice of C programmers, in which underscores, rather than capital letters, separate the “subwords” of names.

6约翰·巴克斯(1924-2007)也是 Fortran 的发明者。他的大部分职业生涯都在 IBM 公司度过,并于 1987 年被任命为 IBM 院士。他于 1977 年获得 ACM 图灵奖。

6 John Backus (1924–2007) was also the inventor of Fortran. He spent most of his professional career at IBM Corporation, and was named an IBM Fellow in 1987. He received the ACM Turing Award in 1977.

7有些作者使用花括号 ({ }) 来表示其中的符号有零个或多个实例。有些作者使用方括号 ([ ]) 来表示其中的符号有零个或一个实例 — 也就是说,表示这些符号是可选的。

7 Some authors use curly braces ({ }) to indicate zero or more instances of the symbols inside. Some use square brackets ([ ]) to indicate zero or one instances of the symbols inside—that is, to indicate that those symbols are optional.

8为了避免混淆,有些作者将定义语言中的任何单个字符放在引号内: id_list → id ( ',' id )*; expr → '(' expr ')'。在常规和扩展 BNF 中,许多作者都使用 ::= 代替 →。

8 To avoid confusion, some authors place quote marks around any single character that is part of the language being defined: id_list → id ( ',' id )*; expr → '(' expr ')'. In both regular and extended BNF, many authors use ::= instead of →.

9给定一个特定的语法,有很多方法可以创建其他等效语法。例如,我们可以将产生式右侧出现的所有A替换为某个新符号B ,然后创建新的产生式B A

9 Given a specific grammar, there are many ways to create other equivalent grammars. We could, for example, replace A with some new symbol B everywhere it appears in the right-hand side of a production, and then create a new production BA.

10许多语言包含预定义标识符(例如,标准库函数的标识符),但这些不是关键字。程序员可以重新定义它们,因此扫描器必须将它们与其他标识符一样对待。同样,上下文关键字必须由扫描器作为标识符处理。

10 Many languages include predefined identifiers (e.g., for standard library functions), but these are not keywords. The programmer can redefine them, so the scanner must treat them the same as other identifiers. Contextual keywords, similarly, must be treated by the scanner as identifiers.

11事实上,水手 1 号软件故障似乎是由于手写笔记中缺少“bar”标点符号(表示平均值)所致,而软件正是从手写笔记中衍生而来 [ Cer89,第 202-203 页]。Fortran DO 循环错误似乎至少在 NASA 的一个软件中出现过,但并未造成严重损害 [ Web89 ]。

11 In actuality, the faulty software for Mariner 1 appears to have stemmed from a missing “bar” punctuation mark (indicating an average) in handwritten notes from which the software was derived [Cer89, pp. 202–203]. The Fortran DO loop error does appear to have occurred in at least one piece of NASA software, but no serious harm resulted [Web89].

12一般而言,如果算法的运行时间t ( n ) 在最坏情况下与 f(n) 成正比,则称该算法的运行时间为O ( f ( n )),其中n是输入的长度。更准确地说,我们说t ( n ) = O ( f ( n )) ⇔ ∃ c , m [ n > m t ( n ) < cf ( n )]。

12 In general, an algorithm is said to run in time O(f(n)), where n is the length of the input, if its running time t(n) is proportional to f (n) in the worst case. More precisely, we say t(n) = O(f(n)) ⇔ ∃c, m [n > mt(n) < cf(n)].

13按照惯例,我们使用靠近字母表开头的大写罗马字母来表示非终结符,靠近字母表结尾的大写罗马字母来表示任意语法符号(终结符或非终结符),靠近字母表开头的小写罗马字母来表示终结符(标记),靠近字母表结尾的小写罗马字母来表示标记字符串,并使用小写希腊字母来表示任意符号的字符串。

13 Following conventional notation, we use uppercase Roman letters near the beginning of the alphabet to represent nonterminals, uppercase Roman letters near the end of the alphabet to represent arbitrary grammar symbols (terminals or nonterminals), lowercase Roman letters near the beginning of the alphabet to represent terminals (tokens), lowercase Roman letters near the end of the alphabet to represent token strings, and lowercase Greek letters to represent strings of arbitrary symbols.

14诺姆·乔姆斯基(1928-),麻省理工学院的语言学家和社会哲学家,发展了许多早期形式语言理论。

14 Noam Chomsky (1928–), a linguist and social philosopher at the Massachusetts Institute of Technology, developed much of the early theory of formal languages.

15达纳·斯科特(1932-),卡内基梅隆大学名誉教授,因发明领域理论和开创指称语义学领域而闻名,该领域提供了一种数学上严谨的方式来形式化编程语言的含义。哈佛大学的迈克尔·拉宾(1931-)对计算机科学中的非确定性和随机性概念做出了开创性的贡献。斯科特和拉宾于 1976 年共同获得 ACM 图灵奖。

15 Dana Scott (1932–), Professor Emeritus at Carnegie Mellon University, is known principally for inventing domain theory and launching the field of denotational semantics, which provides a mathematically rigorous way to formalize the meaning of programming languages. Michael Rabin (1931–), of Harvard University, has made seminal contributions to the concepts of nondeterminism and randomization in computer science. Scott and Rabin shared the ACM Turing Award in 1976.

3

名称、范围和绑定

Names, Scopes, and Bindings

早期的语言(例如 Fortran、Algol 和 Lisp)之所以被称为“高级”语言,是因为它们的语法和语义比它们要取代的汇编语言抽象得多(距离硬件更远)。抽象使得编写可在各种机器上运行良好的程序成为可能,也使程序更容易被人类理解。虽然机器独立性仍然很重要,但编程的简易性仍然是推动现代语言设计的主动力。本章是解决语言设计核心问题的六章中的第一章。(其他三章是第 6 章第 10章。)当前的讨论大部分将围绕名称的概念展开。

Early languages such as Fortran, Algol, and Lisp were termed “high level” because their syntax and semantics were significantly more abstract—farther from the hardware—than those of the assembly languages they were intended to supplant. Abstraction made it possible to write programs that would run well on a wide variety of machines. It also made programs significantly easier for human beings to understand. While machine independence remains important, it is primarily ease of programming that continues to drive the design of modern languages. This chapter is the first of six to address core issues in language design. (The others are Chapters 6 through 10.) Much of the current discussion will revolve around the notion of names.

名称是用于表示其他内容的助记字符串。大多数语言中的名称都是标识符(字母数字标记),但某些其他符号(例如 + 或 :=)也可以是名称。名称允许我们使用符号标识符而不是地址等低级概念来引用变量、常量、操作、类型等。名称在“抽象”一词的第二个含义中也是必不可少的。在这个第二个含义中,抽象是程序员将名称与可能复杂的程序片段相关联的过程,然后可以从其目的或功能的角度来思考,而不是从如何实现该功能的角度来思考。通过隐藏不相关的细节,抽象降低了概念复杂性,使程序员可以在任何特定时间专注于程序文本的可管理子集。子程序是控制抽象:它们允许程序员将任意复杂的代码隐藏在简单的接口后面。类是数据抽象:它们允许程序员将数据表示细节隐藏在(相对)简单的操作集后面。

A name is a mnemonic character string used to represent something else. Names in most languages are identifiers (alphanumeric tokens), though certain other symbols, such as + or :=, can also be names. Names allow us to refer to variables, constants, operations, types, and so on using symbolic identifiers rather than low-level concepts like addresses. Names are also essential in the context of a second meaning of the word abstraction. In this second meaning, abstraction is a process by which the programmer associates a name with a potentially complicated program fragment, which can then be thought of in terms of its purpose or function, rather than in terms of how that function is achieved. By hiding irrelevant details, abstraction reduces conceptual complexity, making it possible for the programmer to focus on a manageable subset of the program text at any particular time. Subroutines are control abstractions: they allow the programmer to hide arbitrarily complicated code behind a simple interface. Classes are data abstractions: they allow the programmer to hide data representation details behind a (comparatively) simple set of operations.

我们将讨论与名称相关的几个主要问题。第 3.1 节介绍了绑定时间的概念,它不仅指将名称绑定到它所代表的事物,而且通常指解决语言实现中的任何设计决策的概念。第 3.2 节概述了用于为对象分配和释放存储空间的各种机制,并区分了对象的生存期和名称与该对象的绑定的生存期。1大多数名称到对象的绑定仅在给定高级程序的有限区域内可用。第 3.3 节探讨了定义此区域的范围规则;第 3.4 节(主要在配套网站上)考虑了它们的实现。

We will look at several major issues related to names. Section 3.1 introduces the notion of binding time, which refers not only to the binding of a name to the thing it represents, but also in general to the notion of resolving any design decision in a language implementation. Section 3.2 outlines the various mechanisms used to allocate and deallocate storage space for objects, and distinguishes between the lifetime of an object and the lifetime of a binding of a name to that object.1 Most name-to-object bindings are usable only within a limited region of a given high-level program. Section 3.3 explores the scope rules that define this region; Section 3.4 (mostly on the companion site) considers their implementation.

在程序中给定点生效的完整绑定集称为当前引用环境。3.5讨论了别名,其中多个名称可能引用给定范围内的给定对象,以及重载,其中一个名称可能引用给定范围内的多个对象,具体取决于引用的上下文。3.6通过考虑引用环境如何绑定到作为参数传递、从函数返回或存储在变量中的子例程,扩展了范围规则的概念。3.7讨论了宏扩展,它可以通过文本替换引入新名称,有时与语言的其余部分不一致。最后,3.8 节(主要在配套站点上)讨论了单独编译。

The complete set of bindings in effect at a given point in a program is known as the current referencing environment. Section 3.5 discusses aliasing, in which more than one name may refer to a given object in a given scope, and overloading, in which a name may refer to more than one object in a given scope, depending on the context of the reference. Section 3.6 expands on the notion of scope rules by considering the ways in which a referencing environment may be bound to a subroutine that is passed as a parameter, returned from a function, or stored in a variable. Section 3.7 discusses macro expansion, which can introduce new names via textual substitution, sometimes in ways that are at odds with the rest of the language. Finally, Section 3.8 (mostly on the companion site) discusses separate compilation.

3.1 绑定时间的概念

3.1 The Notion of Binding Time

绑定是两个事物之间的关联,例如名称和它所命名的事物。绑定时间是创建绑定的时间,或者更一般地说,是做出任何实现决策的时间(我们可以将其视为将答案绑定到问题上)。决策可能在很多不同的时间进行绑定:

A binding is an association between two things, such as a name and the thing it names. Binding time is the time at which a binding is created or, more generally, the time at which any implementation decision is made (we can think of this as binding an answer to a question). There are many different times at which decisions maybe bound:

语言设计时:在大多数语言中,控制流构造、基本(原始)类型集、可用于创建复杂类型的构造函数以及语言语义的许多其他方面都是在设计语言时选择的。

Language design time: In most languages, the control-flow constructs, the set of fundamental (primitive) types, the available constructors for creating complex types, and many other aspects of language semantics are chosen when the language is designed.

语言实现时间:大多数语言手册将各种问题留给语言实现者自行决定。典型(但绝不是普遍)示例包括基本类型的精度(位数)、I/O 与操作系统文件概念的耦合,以及堆栈和堆的组织和最大大小。

Language implementation time: Most language manuals leave a variety of issues to the discretion of the language implementor. Typical (though by no means universal) examples include the precision (number of bits) of the fundamental types, the coupling of I/O to the operating system's notion of files, and the organization and maximum sizes of the stack and heap.

编写程序时间:程序员当然会选择算法、数据结构和名称。

Program writing time: Programmers, of course, choose algorithms, data structures, and names.

编译时:编译器选择高级结构到机器代码的映射,包括内存中静态定义的数据的布局。

Compile time: Compilers choose the mapping of high-level constructs to machine code, including the layout of statically defined data in memory.

链接时间:由于大多数编译器支持单独编译(在不同时间编译程序的不同模块),并且依赖于标准子例程库的可用性,因此程序通常在各个模块通过链接器连接在一起之前是不完整的。链接器会选择模块相对于彼此的整体布局,并解析模块间引用。当一个模块中的名称引用另一个模块中的对象时,两者之间的绑定直到链接时才会最终确定。

Link time: Since most compilers support separate compilation—compiling different modules of a program at different times—and depend on the availability of a library of standard subroutines, a program is usually not complete until the various modules are joined together by a linker. The linker chooses the overall layout of the modules with respect to one another, and resolves intermodule references. When a name in one module refers to an object in another module, the binding between the two is not finalized until link time.

加载时间:加载时间是指操作系统将程序加载到内存中以便运行的时间点。在原始操作系统中,程序中对象的机器地址选择直到加载时才最终确定。大多数现代操作系统区分虚拟地址和物理地址。虚拟地址是在链接时选择的;物理地址实际上可以在运行时更改。处理器的内存管理硬件在运行时的每个单独指令期间将虚拟地址转换为物理地址。

Load time: Load time refers to the point at which the operating system loads the program into memory so that it can run. In primitive operating systems, the choice of machine addresses for objects within the program was not finalized until load time. Most modern operating systems distinguish between virtual and physical addresses. Virtual addresses are chosen at link time; physical addresses can actually change at run time. The processor's memory management hardware translates virtual addresses into physical addresses during each individual instruction at run time.

运行时:运行时实际上是一个非常宽泛的术语,涵盖了从执行开始到结束的整个过程。值与变量的绑定发生在运行时,其他许多因语言而异的决策也是如此。运行时包括程序启动时间、模块进入时间、阐述时间(声明首次“被看到”的时间点)、子例程调用时间、块进入时间以及表达式求值时间/语句执行。

Run time: Run time is actually a very broad term that covers the entire span from the beginning to the end of execution. Bindings of values to variables occur at run time, as do a host of other decisions that vary from language to language. Run time subsumes program start-up time, module entry time, elaboration time (the point at which a declaration is first “seen”), subroutine call time, block entry time, and expression evaluation time/statement execution.

静态动态这两个术语通常分别用于指代在运行前和运行时绑定的事物。显然,“静态”是一个粗略的术语。“动态”也是如此。

The terms static and dynamic are generally used to refer to things bound before run time and at run time, respectively. Clearly “static” is a coarse term. So is “dynamic.”

基于编译器的语言实现往往比基于解释器的实现更高效,因为它们会更早做出决定。例如,编译器会在程序运行之前分析一次全局变量声明的语法和语义。它会决定这些变量在内存中的布局,并生成高效的代码来访问它们,无论它们出现在程序中的何处。相比之下,纯解释器必须在每次程序开始执行时分析声明。在最坏的情况下,解释器可能会在每次调用子例程时重新分析子例程中的本地声明。如果调用出现在深度嵌套的循环中,那么仅分析一次声明的编译器所实现的节省可能非常大。正如我们将在在下一节中,编译器通常无法在编译时预测局部变量的地址,因为变量的空间将在堆栈上动态分配,但它可以安排变量出现在运行时某个寄存器指向的位置的固定偏移量处。

Compiler-based language implementations tend to be more efficient than interpreter-based implementations because they make earlier decisions. For example, a compiler analyzes the syntax and semantics of global variable declarations once, before the program ever runs. It decides on a layout for those variables in memory and generates efficient code to access them wherever they appear in the program. A pure interpreter, by contrast, must analyze the declarations every time the program begins execution. In the worst case, an interpreter may reanalyze the local declarations within a subroutine each time that subroutine is called. If a call appears in a deeply nested loop, the savings achieved by a compiler that is able to analyze the declarations only once may be very large. As we shall see in the following section, a compiler will not usually be able to predict the address of a local variable at compile time, since space for the variable will be allocated dynamically on a stack, but it can arrange for the variable to appear at a fixed offset from the location pointed to by a certain register at run time.

设计与实现

Design & Implementation

3.1 绑定时间

3.1 Binding time

绑定时间在编程语言的设计和实现中的重要性怎么强调都不过分。一般来说,绑定时间越早,效率越高,而绑定时间越晚,灵活性越高。这两个目标之间的矛盾为本书后面的章节提供了一个反复出现的主题。

It is difficult to overemphasize the importance of binding times in the design and implementation of programming languages. In general, early binding times are associated with greater efficiency, while later binding times are associated with greater flexibility. The tension between these goals provides a recurring theme for later chapters of this book.

某些语言难以编译,因为它们的语义要求将基本决策推迟到运行时,这通常是为了增加语言的灵活性或表现力。例如,大多数脚本语言将所有类型检查推迟到运行时。对任意类型(类)对象的引用可以分配给任意命名的变量,只要程序永远不会对未准备好处理的对象应用运算符(调用其方法)即可。这种多态性形式- 适用于多种类型的对象或表达式 - 允许程序员编写异常灵活和通用的代码。我们将在以后的几个部分中再次提到多态性,包括7.1.27.310.1.114.4.4

Some languages are difficult to compile because their semantics require fundamental decisions to be postponed until run time, generally in order to increase the flexibility or expressiveness of the language. Most scripting languages, for example, delay all type checking until run time. References to objects of arbitrary types (classes) can be assigned into arbitrary named variables, as long as the program never ends up applying an operator to (invoking a method of) an object that is not prepared to handle it. This form of polymorphism—applicability to objects or expressions of multiple types—allows the programmer to write unusually flexible and general-purpose code. We will mention polymorphism again in several future sections, including 7.1.2, 7.3, 10.1.1, and 14.4.4.

3.2 对象生命周期和存储管理

3.2 Object Lifetime and Storage Management

在任何有关名称和绑定的讨论中,区分名称和它们所引用的对象并识别几个关键事件非常重要:

In any discussion of names and bindings, it is important to distinguish between names and the objects to which they refer, and to identify several key events:

 对象的创建和销毁

 Creation and destruction of objects

 绑定的创建和销毁

 Creation and destruction of bindings

 停用和重新激活可能暂时无法使用的绑定

 Deactivation and reactivation of bindings that may be temporarily unusable

 对变量、子例程、类型等的引用,所有这些都使用绑定

 References to variables, subroutines, types, and so on, all of which use bindings

名称到对象绑定的创建和销毁之间的时间段称为绑定的生命周期。类似地,对象的创建和销毁之间的时间是对象的生命周期。这些生命周期不一定需要一致。特别是,即使给定的名称不再可用于访问对象,对象仍可以保留其值和被访问的潜力。例如,当变量通过引用传递给子例程时(通常在 Fortran 中或 C++ 中使用“ & ”参数),参数名称和传递的变量之间的绑定的生命周期短于变量本身的生命周期。名称到对象绑定的生命周期也可能长于对象的生命周期,尽管这通常是程序错误的标志。例如,如果通过 C++ new运算符创建的对象作为&参数传递,然后在子例程返回之前释放(delete),则会发生这种情况。对不再存在的对象的绑定称为悬垂引用。悬垂引用将在第 3.6 节8.5.2节中进一步讨论。

The period of time between the creation and the destruction of a name-to-object binding is called the binding's lifetime. Similarly, the time between the creation and destruction of an object is the object's lifetime. These lifetimes need not necessarily coincide. In particular, an object may retain its value and the potential to be accessed even when a given name can no longer be used to access it. When a variable is passed to a subroutine by reference, for example (as it typically is in Fortran or with '&' parameters in C++), the binding between the parameter name and the variable that was passed has a lifetime shorter than that of the variable itself. It is also possible, though generally a sign of a program bug, for a name-to-object binding to have a lifetime longer than that of the object. This can happen, for example, if an object created via the C++ new operator is passed as a & parameter and then deallocated (delete-ed) before the subroutine returns. A binding to an object that is no longer live is called a dangling reference. Dangling references will be discussed further in Sections 3.6 and 8.5.2.

对象生命周期通常对应于三种主要存储分配机制之一,用于管理对象的空间:

Object lifetimes generally correspond to one of three principal storage allocation mechanisms, used to manage the object's space:

1. 静态对象被赋予一个绝对地址,该地址在整个程序执行过程中都保留下来。

1. Static objects are given an absolute address that is retained throughout the program's execution.

2. 堆栈对象按照后进先出的顺序进行分配和释放,通常与子程序调用和返回结合使用。

2. Stack objects are allocated and deallocated in last-in, first-out order, usually in conjunction with subroutine calls and returns.

3. 对象可能在任意时间被分配和释放。它们需要更通用(且昂贵)的存储管理算法。

3. Heap objects maybe allocated and deallocated at arbitrary times. They require a more general (and expensive) storage management algorithm.

3.2.1 静态分配

3.2.1 Static Allocation

全局变量是静态对象的明显示例,但不是唯一的示例。构成程序机器码的指令也可以被视为静态分配的对象。我们将在3.3.1 节中看到一些示例,这些示例是单个子例程的本地变量,但它们的值在一次调用到下一次调用时保持不变;它们的空间是静态分配的。数字和字符串值常量文字也是静态分配的,例如A = B/14.7printf(“hello, world\n”)之类的语句。(小常量通常存储在指令本身内;较大的常量则分配到单独的位置。)最后,大多数编译器会生成各种表,供运行时支持例程用于调试、动态类型检查、垃圾收集、异常处理和其他目的;这些也是静态分配的。静态分配的对象(例如,指令、常量和某些运行时表)的值在程序执行期间不应改变,通常分配在受保护的只读内存中,因此任何无意的写入尝试都会导致处理器中断,从而允许操作系统宣布运行时错误。

Global variables are the obvious example of static objects, but not the only one. The instructions that constitute a program's machine code can also be thought of as statically allocated objects. We shall see examples in Section 3.3.1 of variables that are local to a single subroutine, but retain their values from one invocation to the next; their space is statically allocated. Numeric and string-valued constant literals are also statically allocated, for statements such as A = B/14.7 or printf(“hello, world\n”). (Small constants are often stored within the instruction itself; larger ones are assigned a separate location.) Finally, most compilers produce a variety of tables that are used by run-time support routines for debugging, dynamic type checking, garbage collection, exception handling, and other purposes; these are also statically allocated. Statically allocated objects whose value should not change during program execution (e.g., instructions, constants, and certain run-time tables) are often allocated in protected, read-only memory, so that any inadvertent attempt to write to them will cause a processor interrupt, allowing the operating system to announce a run-time error.

例 3.1

Example 3.1

局部变量的静态分配

Static allocation of local variables

从逻辑上讲,局部变量在调用其子例程时创建,并在返回时销毁。如果重复调用子例程,则每次调用都会创建和销毁每个局部变量的单独实例但是,语言实现并不总是必须在运行时执行与这些创建和销毁操作相对应的工作。Fortran 最初不支持递归(它是在 Fortran 90 中添加的)。因此,在较旧的 Fortran 程序中,任何时候都不能有多个子例程处于活动状态,并且编译器可以选择对局部变量使用静态分配,有效地安排不同调用的变量共享相同的位置,从而避免创建和销毁的任何运行时开销。■

Logically speaking, local variables are created when their subroutine is called, and destroyed when it returns. If the subroutine is called repeatedly, each invocation is said to create and destroy a separate instance of each local variable. It is not always the case, however, that a language implementation must perform work at run time corresponding to these create and destroy operations. Recursion was not originally supported in Fortran (it was added in Fortran 90). As a result, there can never be more than one invocation of a subroutine active in an older Fortran program at any given time, and a compiler may choose to use static allocation for local variables, effectively arranging for the variables of different invocations to share the same locations, and thereby avoiding any run-time overhead for creation and destruction. ■

设计与实现

Design & Implementation

3.2 Fortran 中的递归

3.2 Recursion in Fortran

Fortran 90 之前版本没有递归,这通常归因于该语言最初实现的 IBM 704 上堆栈操作的开销。许多(或许是大多数)Fortran 实现选择使用堆栈来处理局部变量,但由于语言定义允许使用静态分配,Fortran 程序员在 30 多年里无法享受语言支持的递归带来的好处。

The lack of recursion in (pre-Fortran 90) Fortran is generally attributed to the expense of stack manipulation on the IBM 704, on which the language was first implemented. Many (perhaps most) Fortran implementations choose to use a stack for local variables, but because the language definition permits the use of static allocation instead, Fortran programmers were denied the benefits of language-supported recursion for over 30 years.

在许多语言中,命名常量必须具有可在编译时确定的值。通常,指定常量值的表达式只允许包含其他已知常量、内置函数和算术运算符。这种命名常量与常量文字有时称为清单常量编译时常量。清单常量始终可以静态分配,即使它们对于递归子例程来说是本地的:多个实例可以共享同一位置。

In many languages a named constant is required to have a value that can be determined at compile time. Usually the expression that specifies the constant's value is permitted to include only other known constants and built-in functions and arithmetic operators. Named constants of this sort, together with constant literals, are sometimes called manifest constants or compile-time constants. Manifest constants can always be allocated statically, even if they are local to a recursive subroutine: multiple instances can share the same location.

在其他语言(例如 C 和 Ada)中,常量只是在阐述(初始化)时间之后无法更改的变量。它们的值虽然不变,但有时可能取决于在运行时才知道的其他值。当此类阐述时间常量位于递归子例程的本地时,必须在堆栈上分配。C# 分别使用constreadonly关键字来区分编译时常量和阐述时间常量。

In other languages (e.g., C and Ada), constants are simply variables that cannot be changed after elaboration (initialization) time. Their values, though unchanging, can sometimes depend on other values that are not known until run time. Such elaboration-time constants, when local to a recursive subroutine, must be allocated on the stack. C# distinguishes between compile-time and elaboration-time constants using the const and readonly keywords, respectively.

3.2.2 基于堆栈的分配

3.2.2 Stack-Based Allocation

例 3.2

Example 3.2

运行时堆栈的布局

Layout of the run-time stack

如果一种语言允许递归,那么局部变量的静态分配就不再是一种选择,因为从概念上讲,可能需要同时存在的变量实例数是无限的。幸运的是,子程序调用的自然嵌套使得在堆栈上为局部变量分配空间变得很容易。图 3.1显示了典型堆栈的简化图。运行时的每个子程序实例在堆栈上都有自己的框架(也称为激活记录),其中包含参数和返回值、局部变量、临时变量和簿记信息。临时变量通常是复杂计算中产生的中间值。簿记信息通常包括子程序的返回地址、对调用者堆栈框架的引用(也称为动态链接、调用者和被调用者都需要的寄存器的保存值以及我们稍后将研究的其他各种值。要传递给后续例程的参数位于框架的顶部,被调用者可以轻松找到它们。剩余信息的组织与实现相关:它因语言、机器和编译器的不同而不同。■

If a language permits recursion, static allocation of local variables is no longer an option, since the number of instances of a variable that may need to exist at the same time is conceptually unbounded. Fortunately, the natural nesting of subroutine calls makes it easy to allocate space for locals on a stack. A simplified picture of a typical stack appears in Figure 3.1. Each instance of a subroutine at run time has its own frame (also called an activation record) on the stack, containing arguments and return values, local variables, temporaries, and bookkeeping information. Temporaries are typically intermediate values produced in complex calculations. Bookkeeping information typically includes the subroutine's return address, a reference to the stack frame of the caller (also called the dynamic link), saved values of registers needed by both the caller and the callee, and various other values that we will study later. Arguments to be passed to subsequent routines lie at the top of the frame, where the callee can easily find them. The organization of the remaining information is implementation-dependent: it varies from one language, machine, and compiler to another. ■

编号03-01-9780124104099
图 3.1 基于堆栈的子程序空间分配。我们在此假设子程序已被调用,如右上图所示。特别是,B 在调用 C 之前已递归调用了自身一次。如果 D 返回并且 C 调用 E,则 E 的框架(激活记录)将占据先前用于 D 的框架的相同空间。在任何给定时间,堆栈指针 ( sp ) 寄存器指向堆栈上第一个未使用的位置(或某些机器上最后使用的位置),而框架指针 ( fp ) 寄存器指向当前子程序框架内的已知位置。框架内字段的相对顺序可能因机器和编译器而异

堆栈的维护是子程序调用序列(调用者在调用前后立即执行的代码)以及子程序本身的序言(在开头执行的代码)和尾声(在结尾执行的代码)的责任。有时术语“调用序列”用于指调用者、序言和尾声的组合操作。我们将在第 9.2 节中更详细地研究调用序列。

Maintenance of the stack is the responsibility of the subroutine calling sequence—the code executed by the caller immediately before and after the call—and of the prologue (code executed at the beginning) and epilogue (code executed at the end) of the subroutine itself. Sometimes the term “calling sequence” is used to refer to the combined operations of the caller, the prologue, and the epilogue. We will study calling sequences in more detail in Section 9.2.

虽然无法在编译时预测堆栈帧的位置(编译器通常无法判断堆栈中可能已经有哪些其他帧),但帧内对象的偏移量通常可以静态确定。此外,编译器可以安排(在调用序列或序言中)特定寄存器(称为帧指针)始终指向当前子例程帧内的已知位置。需要访问当前帧内的局部变量或调用帧顶部附近的参数的代码可以通过将预定的偏移量添加到帧指针中的值来实现。正如我们在 C-5.3.1 节中讨论的那样,几乎每个处理器都提供了一种位移寻址机制,允许将此添加隐式指定为普通加载存储指令的一部分。在大多数语言实现中,堆栈向较低地址“向下”增长。一些机器提供特殊的推送弹出指令来假设这种增长方向。局部变量、临时变量和簿记信息通常与帧指针具有负偏移量。参数和返回通常具有正偏移量;它们驻留在呼叫者的框架中。

While the location of a stack frame cannot be predicted at compile time (the compiler cannot in general tell what other frames may already be on the stack), the offsets of objects within a frame usually can be statically determined. Moreover, the compiler can arrange (in the calling sequence or prologue) for a particular register, known as the frame pointer to always point to a known location within the frame of the current subroutine. Code that needs to access a local variable within the current frame, or an argument near the top of the calling frame, can do so by adding a predetermined offset to the value in the frame pointer. As we discuss in Section C-5.3.1, almost every processor provides a displacement addressing mechanism that allows this addition to be specified implicitly as part of an ordinary load or store instruction. The stack grows “downward” toward lower addresses in most language implementations. Some machines provide special push and pop instructions that assume this direction of growth. Local variables, temporaries, and bookkeeping information typically have negative offsets from the frame pointer. Arguments and returns typically have positive offsets; they reside in the caller's frame.

即使在没有递归的语言中,使用堆栈来存储局部变量也比静态分配局部变量更有优势。在大多数程序中,子程序之间的潜在调用模式不允许所有这些子程序同时处于活动状态。因此,当前活动子程序的局部变量所需的总空间很少与所有子程序(无论是否活动)的总空间一样大。因此,堆栈在运行时所需的内存可能比静态分配所需的内存少得多。

Even in a language without recursion, it can be advantageous to use a stack for local variables, rather than allocating them statically. In most programs the pattern of potential calls among subroutines does not permit all of those subroutines to be active at the same time. As a result, the total space needed for local variables of currently active subroutines is seldom as large as the total space across all subroutines, active or not. A stack may therefore require substantially less memory at run time than would be required for static allocation.

3.2.3 基于堆的分配

3.2.3 Heap-Based Allocation

堆是存储区域,可以在任意时间分配和释放子块。2动态分配的链接数据结构片段以及完全通用字符串、列表和集合等对象都需要堆,这些对象的大小可能会因赋值语句或其他更新操作而发生变化。

A heap is a region of storage in which subblocks can be allocated and deallocated at arbitrary times.2 Heaps are required for the dynamically allocated pieces of linked data structures, and for objects such as fully general character strings, lists, and sets, whose size may change as a result of an assignment statement or other update operation.

例 3.3

Example 3.3

堆中的外部碎片

External fragmentation in the heap

有许多可能的策略来管理堆中的空间。我们在这里回顾主要的替代方案;详细信息可以在任何数据结构教科书中找到。主要关注的是速度和空间,和往常一样,它们之间存在权衡。空间问题可以进一步细分为内部碎片和外部碎片问题。当存储管理算法分配的块大于保存给定对象所需的块时,就会发生内部碎片;多余的空间则未使用。当已分配给活动对象的块分散在堆中,以至于剩余的未使用空间由多个块组成时,就会发生外部碎片:可能有相当多的可用空间,但其中没有一块可能大到足以满足未来的某些请求(参见图 3.2)。■

There are many possible strategies to manage space in a heap. We review the major alternatives here; details can be found in any data-structures textbook. The principal concerns are speed and space, and as usual there are tradeoffs between them. Space concerns can be further subdivided into issues of internal and external fragmentation. Internal fragmentation occurs when a storage-management algorithm allocates a block that is larger than required to hold a given object; the extra space is then unused. External fragmentation occurs when the blocks that have been assigned to active objects are scattered through the heap in such a way that the remaining, unused space is composed of multiple blocks: there may be quite a lot of free space, but no one piece of it may be large enough to satisfy some future request (see Figure 3.2). ■

传真:03-02-9780124104099
图 3.2 碎片。阴影块正在使用;空白块是空闲的。正在使用块末端的交叉阴影空间表示内部碎片。不连续的空闲块表示外部碎片。虽然剩余的总空闲空间足以满足图示大小的分配请求,但没有一个剩余的块足够大。

许多存储管理算法维护一个链表,即空闲列表,其中包含当前未使用的堆块。最初,该列表由一个包含整个堆的块组成。每次分配请求时,算法都会在列表中搜索合适大小的块。使用首次适合算法,我们选择列表中第一个足够大以满足请求的块。使用最佳适合算法,我们搜索整个列表以找到足够大的最小块以满足请求。无论哪种情况,如果所选块明显大于所需块,我们都会将其分成两部分,并将不需要的部分作为较小的块返回到空闲列表。(如果不需要的部分大小低于某个最小阈值,我们可能会将其作为内部碎片留在已分配的块中。)当一个块被释放并返回到空闲列表时,我们会检查物理相邻的块中是否有一个或两个是空闲的;如果是,我们会将它们合并起来。

Many storage-management algorithms maintain a single linked list—the free list—of heap blocks not currently in use. Initially the list consists of a single block comprising the entire heap. At each allocation request the algorithm searches the list for a block of appropriate size. With a first fit algorithm we select the first block on the list that is large enough to satisfy the request. With a best fit algorithm we search the entire list to find the smallest block that is large enough to satisfy the request. In either case, if the chosen block is significantly larger than required, then we divide it into two and return the unneeded portion to the free list as a smaller block. (If the unneeded portion is below some minimum threshold in size, we may leave it in the allocated block as internal fragmentation.) When a block is deallocated and returned to the free list, we check to see whether either or both of the physically adjacent blocks are free; if so, we coalesce them.

直观地看,人们会认为最佳匹配算法能够更好地为大型请求保留大型块。同时,它的分配成本比首次匹配算法更高,因为它必须始终搜索整个列表,并且往往会导致大量非常小的“剩余”块。哪种方法(首次匹配或最佳匹配)可降低外部碎片率取决于大小请求的分布。

Intuitively, one would expect a best fit algorithm to do a better job of reserving large blocks for large requests. At the same time, it has higher allocation cost than a first fit algorithm, because it must always search the entire list, and it tends to result in a larger number of very small “left-over” blocks. Which approach—first fit or best fit—results in lower external fragmentation depends on the distribution of size requests.

在任何维护单个空闲列表的算法中,分配成本与空闲块的数量成线性关系。为了将此成本降低为常数,某些存储管理算法为不同大小的块维护单独的空闲列表。每个请求都四舍五入到下一个标准大小(以内部碎片为代价)并从适当的列表中分配。实际上,堆被分成“池”,每个标准大小一个。划分可以是静态的,也可以是动态的。两种常见的动态池调整机制称为伙伴系统斐波那契堆。在伙伴系统中,标准块大小是 2 的幂。如果需要一个大小为 2k 的块但没有可用的块,则将大小为 2k+1 的块一分为。其中一半用于满足请求;另一半放在第k个空闲列表中。当一个块被释放时,如果其“伙伴”(创建它的分割的另一半)空闲,它会与该伙伴合并。斐波那契堆与之类似,但标准大小使用斐波那契数,而不是 2 的幂。该算法稍微复杂一些,但内部碎片化程度略低,因为斐波那契数列的增长速度比 2k

In any algorithm that maintains a single free list, the cost of allocation is linear in the number of free blocks. To reduce this cost to a constant, some storage management algorithms maintain separate free lists for blocks of different sizes. Each request is rounded up to the next standard size (at the cost of internal fragmentation) and allocated from the appropriate list. In effect, the heap is divided into “pools,” one for each standard size. The division may be static or dynamic. Two common mechanisms for dynamic pool adjustment are known as the buddy system and the Fibonacci heap. In the buddy system, the standard block sizes are powers of two. If a block of size 2k is needed, but none is available, a block of size 2k+1 is split in two. One of the halves is used to satisfy the request; the other is placed on the kth free list. When a block is deallocated, it is coalesced with its “buddy”—the other half of the split that created it—if that buddy is free. Fibonacci heaps are similar, but use Fibonacci numbers for the standard sizes, instead of powers of two. The algorithm is slightly more complex, but leads to slightly lower internal fragmentation, because the Fibonacci sequence grows more slowly than 2k.

外部碎片的问题在于,堆满足请求的能力可能会随着时间的推移而下降。多个空闲列表可能会有所帮助,因为它们将小块聚集在相对接近的物理位置,但它们并不能消除问题。即使所需的总空间小于堆的大小,也总是有可能设计出无法满足的请求序列。如果内存在大小池之间静态划分,则只需超过给定大小的最大请求数即可。如果池是动态重新调整的,则可以按物理地址的顺序分配大量小块,然后释放每个其他小块,从而“棋盘化”堆,留下小块空闲和已分配的交替模式。为了消除外部碎片,我们必须准备好通过移动已分配的块来压缩堆。这项任务很复杂,因为需要查找和更新对正在移动的块的所有未完成引用。我们将在第8.5.3 节中进一步讨论压缩。

The problem with external fragmentation is that the ability of the heap to satisfy requests may degrade over time. Multiple free lists may help, by clustering small blocks in relatively close physical proximity, but they do not eliminate the problem. It is always possible to devise a sequence of requests that cannot be satisfied, even though the total space required is less than the size of the heap. If memory is partitioned among size pools statically, one need only exceed the maximum number of requests of a given size. If pools are dynamically readjusted, one can “checkerboard” the heap by allocating a large number of small blocks and then deallocating every other one, in order of physical address, leaving an alternating pattern of small free and allocated blocks. To eliminate external fragmentation, we must be prepared to compact the heap, by moving already-allocated blocks. This task is complicated by the need to find and update all outstanding references to a block that is being moved. We will discuss compaction further in Section 8.5.3.

3.2.4 垃圾收集

3.2.4 Garbage Collection

基于堆的对象分配总是由程序中的某些特定操作触发:实例化一个对象、附加到列表的末尾、将一个长值赋给之前的短字符串等等。在某些语言(例如 C、C++ 和 Rust)中,释放也是显式的。然而,正如我们将在第8.5 节中看到的那样,许多语言指定当无法再从任何程序变量访问对象时,应隐式释放对象。这种语言的运行时库必须提供垃圾收集机制来识别和回收无法访问的对象。大多数函数式和脚本语言都需要垃圾收集,许多较新的命令式语言(包括 Java 和 C#)也是如此。

Allocation of heap-based objects is always triggered by some specific operation in a program: instantiating an object, appending to the end of a list, assigning a long value into a previously short string, and so on. Deallocation is also explicit in some languages (e.g., C, C++, and Rust). As we shall see in Section 8.5, however, many languages specify that objects are to be deallocated implicitly when it is no longer possible to reach them from any program variable. The run-time library for such a language must then provide a garbage collection mechanism to identify and reclaim unreachable objects. Most functional and scripting languages require garbage collection, as do many more recent imperative languages, including Java and C#.

支持显式释放的传统论点是实现简单性和执行速度。即使是简单的自动垃圾收集实现也会给具有丰富类型系统的语言的实现增加相当大的复杂性,即使是最复杂的垃圾收集器也会在某些程序中消耗大量时间。如果程序员能够正确识别对象生命周期的结束,而无需进行太多的运行时记账,那么执行速度可能会更快。

The traditional arguments in favor of explicit deallocation are implementation simplicity and execution speed. Even naive implementations of automatic garbage collection add significant complexity to the implementation of a language with a rich type system, and even the most sophisticated garbage collector can consume nontrivial amounts of time in certain programs. If the programmer can correctly identify the end of an object's lifetime, without too much run-time bookkeeping, the result is likely to be faster execution.

但是,支持自动垃圾收集的理由是令人信服的:手动释放错误是实际程序中最常见且代价最高的错误之一。如果过早释放某个对象,程序可能会遵循悬垂引用,访问现在由另一个对象使用的内存。如果对象在其生命周期结束时释放,则程序可能会“泄漏内存”,最终耗尽堆空间。释放错误非常难以识别和修复。随着时间的推移,许多语言设计者和程序员开始将自动垃圾收集视为语言的一项基本特性。垃圾收集算法已经得到改进,从而降低了运行时开销;语言实现总体上变得更加复杂,从而降低了自动收集的边际复杂性;并且尖端应用程序变得更大、更复杂,使得自动收集的好处更引人注目。

The argument in favor of automatic garbage collection, however, is compelling: manual deallocation errors are among the most common and costly bugs in real-world programs. If an object is deallocated too soon, the program may follow a dangling reference, accessing memory now used by another object. If an object is not deallocated at the end of its lifetime, then the program may “leak memory,” eventually running out of heap space. Deallocation errors are notoriously difficult to identify and fix. Over time, many language designers and programmers have come to consider automatic garbage collection an essential language feature. Garbage-collection algorithms have improved, reducing their run-time overhead; language implementations have become more complex in general, reducing the marginal complexity of automatic collection; and leading-edge applications have become larger and more complex, making the benefits of automatic collection ever more compelling.

03-01-9780124104099检查你的理解

Check Your Understanding

1. 什么是绑定时间

1. What is binding time?

2. 解释静态绑定的决策和动态绑定的决策之间的区别。

2. Explain the distinction between decisions that are bound statically and those that are bound dynamically.

3. 尽早绑定有什么好处?延迟绑定有什么好处?

3. What is the advantage of binding things as early as possible? What is the advantage of delaying bindings?

4.解释 名称到对象绑定的生命周期与其可见性之间的区别。

4. Explain the distinction between the lifetime of a name-to-object binding and its visibility.

5. 什么决定对象是静态分配、在堆栈上还是在堆中?

5. What determines whether an object is allocated statically, on the stack, or in the heap?

6. 列出堆栈框架中常见的对象和信息。

6. List the objects and information commonly found in a stack frame.

7. 什么是帧指针?它有什么用?

7. What is a frame pointer?. What is it used for?

8. 什么是调用序列

8. What is a calling sequence?

9. 什么是内部碎片和外部碎片

9. What are internal and external fragmentation?

10. 什么是垃圾收集

10. What is garbage collection?

11. 什么是悬垂引用

11. What is a dangling reference?

3.3 范围规则

3.3 Scope Rules

绑定处于活动状态的程序文本区域即其作用域。在大多数现代语言中,绑定的作用域是静态确定的,即在编译时确定。例如,在 C 语言中,我们在进入子例程时引入新的作用域。我们为本地对象创建绑定,并停用被同名本地对象隐藏(不可见)的全局对象的绑定。在退出子例程时,我们销毁本地变量的绑定并重新激活任何隐藏的全局对象的绑定。这些绑定操作乍一看似乎是运行时操作,但它们不需要执行任何代码:绑定处于活动状态的程序部分完全在编译时确定。我们可以查看 C 程序,并根据纯文本规则知道哪些名称在程序的哪个位置引用哪些对象。因此,C 被称为静态作用域(有些作者说是词法作用域3)。其他语言(包括 APL、Snobol、Tcl 和 Lisp 的早期方言)都是动态作用域的:它们的绑定取决于运行时的执行流程。我们将在第 3.3.1 节第 3.3.6节中更详细地介绍静态和动态作用域。

The textual region of the program in which a binding is active is its scope. In most modern languages, the scope of a binding is determined statically, that is, at compile time. In C, for example, we introduce a new scope upon entry to a subroutine. We create bindings for local objects and deactivate bindings for global objects that are hidden (made invisible) by local objects of the same name. On subroutine exit, we destroy bindings for local variables and reactivate bindings for any global objects that were hidden. These manipulations of bindings may at first glance appear to be run-time operations, but they do not require the execution of any code: the portions of the program in which a binding is active are completely determined at compile time. We can look at a C program and know which names refer to which objects at which points in the program based on purely textual rules. For this reason, C is said to be statically scoped (some authors say lexically scoped3). Other languages, including APL, Snobol, Tcl, and early dialects of Lisp, are dynamically scoped: their bindings depend on the flow of execution at run time. We will examine static and dynamic scoping in more detail in Sections 3.3.1 and 3.3.6.

除了谈论“绑定的范围”之外,我们有时还会将“范围”一词单独用作名词,而不考虑特定的绑定。非正式地说,范围是一个最大大小的程序区域,其中没有绑定发生变化(或至少没有绑定被破坏 - 更多信息请参见第 3.3.3 节)。通常,范围是模块、类、子例程或结构化控制流语句的主体,有时称为。在 C 系列语言中,它将用 {…} 括号分隔。

In addition to talking about the “scope of a binding,” we sometimes use the word “scope” as a noun all by itself, without a specific binding in mind. Informally, a scope is a program region of maximal size in which no bindings change (or at least none are destroyed—more on this in Section 3.3.3). Typically, a scope is the body of a module, class, subroutine, or structured control-flow statement, sometimes called a block. In C family languages it would be delimited with {…} braces.

Algol 68 和 Ada 使用术语“精化”来指代控制首次进入范围时声明变为活动的过程。精化需要创建绑定。在许多语言中,它还需要为本地对象分配堆栈空间,并可能分配初始值。在 Ada 中,它可能涉及许多其他事情,包括执行错误检查或堆空间分配代码、传播异常以及创建并发执行的任务(将在第 13 章中讨论)。

Algol 68 and Ada use the term elaboration to refer to the process by which declarations become active when control first enters a scope. Elaboration entails the creation of bindings. In many languages, it also entails the allocation of stack space for local objects, and possibly the assignment of initial values. In Ada it can entail a host of other things, including the execution of error-checking or heap-space-allocating code, the propagation of exceptions, and the creation of concurrently executing tasks (to be discussed in Chapter 13).

在程序执行的任何给定点,活动绑定的集合称为当前引用环境。该集合主要由静态或动态范围规则决定。我们将看到,引用环境通常对应于一系列范围,可以按顺序检查这些范围以找到给定名称的当前绑定。

At any given point in a program's execution, the set of active bindings is called the current referencing environment. The set is principally determined by static or dynamic scope rules. We shall see that a referencing environment generally corresponds to a sequence of scopes that can be examined (in order) to find the current binding for a given name.

在某些情况下,引用环境还取决于(术语使用令人困惑的)绑定规则。具体而言,当将对子程序S的引用存储在变量中、作为参数传递给另一个子程序或作为函数值返回时,需要确定何时选择S的引用环境- 即何时在对S 的引用和S的引用环境之间进行绑定。两个主要选项是深度绑定(在首次创建引用时进行选择)和浅绑定(在最终使用引用时进行选择)。我们将在第 3.6 节中更详细地研究这些选项。

In some cases, referencing environments also depend on what are (in a confusing use of terminology) called binding rules. Specifically, when a reference to a subroutine S is stored in a variable, passed as a parameter to another subroutine, or returned as a function value, one needs to determine when the referencing environment for S is chosen—that is, when the binding between the reference to S and the referencing environment of S is made. The two principal options are deep binding, in which the choice is made when the reference is first created, and shallow binding, in which the choice is made when the reference is finally used. We will examine these options in more detail in Section 3.6.

3.3.1 静态作用域

3.3.1 Static Scoping

在具有静态(词法)作用域的语言中,名称和对象之间的绑定可以在编译时通过检查程序文本来确定,而无需考虑运行时的控制流。通常,给定名称的“当前”绑定位于匹配声明中,该声明的块最接近程序中的给定点,尽管我们将看到,这个基本主题有很多变体。

In a language with static (lexical) scoping, the bindings between names and objects can be determined at compile time by examining the text of the program, without consideration of the flow of control at run time. Typically, the “current” binding for a given name is found in the matching declaration whose block most closely surrounds a given point in the program, though as we shall see there are many variants on this basic theme.

最简单的静态作用域规则可能是 Basic 的早期版本,其中只有一个全局作用域。事实上,只有几百个可能的名称,每个名称都由一个字母和一个数字组成。没有显式声明;变量通过使用而隐式声明。

The simplest static scope rule is probably that of early versions of Basic, in which there was only a single, global scope. In fact, there were only a few hundred possible names, each of which consisted of a letter optionally followed by a digit. There were no explicit declarations; variables were declared implicitly by virtue of being used.

在 Fortran 90 之前的版本中,范围规则稍微复杂一些,但也没有那么复杂。Fortran 区分全局变量和局部变量。局部变量的范围仅限于它出现的子程序;它在其他地方不可见。变量声明是可选的。如果未声明变量,则假定它是当前子程序的局部变量,并且如果其名称以字母 I-N 开头,则为整型,否则为实型。(程序员可以指定不同的隐式声明约定。在 Fortran 90 及其后续版本中,程序员还可以关闭隐式声明,这样使用未声明的变量就会成为编译时错误。)

Scope rules are somewhat more complex in (pre-Fortran 90) Fortran, though not much more. Fortran distinguishes between global and local variables. The scope of a local variable is limited to the subroutine in which it appears; it is not visible elsewhere. Variable declarations are optional. If a variable is not declared, it is assumed to be local to the current subroutine and to be of type integer if its name begins with the letters I–N, or real otherwise. (Different conventions for implicit declarations can be specified by the programmer. In Fortran 90 and its successors, the programmer can also turn off implicit declarations, so that use of an undeclared variable becomes a compile-time error.)

从语义上讲,局部 Fortran 变量(对象本身和名称到对象的绑定)的生存期包含变量子例程的一次执行。程序员可以使用显式 save 语句来覆盖此规则。(许多其他语言中也存在类似的机制:在 C 中,将变量声明为static;在 Algol 中,将变量声明为own。)保存的staticown)变量的生存期包含程序的整个执行。编译器不会为每次调用子例程创建一个逻辑上独立的对象,而是创建一个对象,该对象在从一次子例程调用到下一次子例程调用之间保留其值。(当然,当子例程未执行时,名称到变量的绑定处于非活动状态,因为名称超出了范围。)

Semantically, the lifetime of a local Fortran variable (both the object itself and the name-to-object binding) encompasses a single execution of the variable's subroutine. Programmers can override this rule by using an explicit save statement. (Similar mechanisms appear in many other languages: in C one declares the variable static; in Algol one declares it own.) A save-ed (static, own) variable has a lifetime that encompasses the entire execution of the program. Instead of a logically separate object for every invocation of the subroutine, the compiler creates a single object that retains its value from one invocation of the subroutine to the next. (The name-to-variable binding, of course, is inactive when the subroutine is not executing, because the name is out of scope.)

例 3.4

Example 3.4

C 中的静态变量

Static variables in C

作为使用静态变量的一个例子,请考虑图 3.3中的代码。子程序label_name可用于生成一系列不同的字符串名称:L1L2、……。编译器可能会在其汇编语言输出中使用这些名称。■

As an example of the use of static variables, consider the code in Figure 3.3. The subroutine label_name can be used to generate a series of distinct character-string names: L1, L2, …. A compiler might use these names in its assembly language output. ■

03-03-9780124104099
图3.3 C代码说明静态变量的使用。

3.3.2 嵌套子程序

3.3.2 Nested Subroutines

Algol 60 中引入了子程序相互嵌套的功能,这是许多后续语言的功能,包括 Ada、ML、Common Lisp、Python、Scheme、Swift 和(在有限范围内)Fortran 90。其他语言(包括 C 及其后代)允许类或其他范围嵌套。正如 Fortran 子程序的局部变量对其他子程序不可见一样,在 Algol 系列语言中,在范围内声明的任何常量、类型、变量或子程序在该范围之外都是不可见的。更正式地说,Algol 风格的嵌套产生了从名称到对象的绑定的最近嵌套范围规则:在声明中引入的名称在声明它的范围中是已知的,并且在每个内部嵌套的范围中都是已知的,除非它被一个或多个嵌套范围中的另一个同名声明隐藏。要找到与名称的给定用法相对应的对象,我们会在当前最内层范围内查找具有该名称的声明。如果有,则它定义了名称的活动绑定。否则,我们在紧邻的范围内寻找声明。我们继续向外,依次检查周围的范围,直到到达程序的外层嵌套级别,其中声明了全局对象。如果在任何级别都找不到声明,则程序有错误。

The ability to nest subroutines inside each other, introduced in Algol 60, is a feature of many subsequent languages, including Ada, ML, Common Lisp, Python, Scheme, Swift, and (to a limited extent) Fortran 90. Other languages, including C and its descendants, allow classes or other scopes to nest. Just as the local variables of a Fortran subroutine are not visible to other subroutines, any constants, types, variables, or subroutines declared within a scope are not visible outside that scope in Algol-family languages. More formally, Algol-style nesting gives rise to the closest nested scope rule for bindings from names to objects: a name that is introduced in a declaration is known in the scope in which it is declared, and in each internally nested scope, unless it is hidden by another declaration of the same name in one or more nested scopes. To find the object corresponding to a given use of a name, we look for a declaration with that name in the current, innermost scope. If there is one, it defines the active binding for the name. Otherwise, we look for a declaration in the immediately surrounding scope. We continue outward, examining successively surrounding scopes, until we reach the outer nesting level of the program, where global objects are declared. If no declaration is found at any level, then the program is in error.

许多语言都提供了内置预定义对象的集合,如 I/O 例程、数学函数,在某些情况下还包括整数字符等类型。通常认为这些对象是在额外的、不可见的最外层作用域中声明的,该作用域围绕着声明全局对象的作用域。上一段中描述的绑定搜索终止于这个额外的、最外层作用域(如果存在),而不是声明全局对象的作用域。这种最外层作用域约定使程序员可以定义一个全局对象,其名称与某个预定义对象相同(其“声明”因此被隐藏,使其不可见)。

Many languages provide a collection of built-in, or predefined objects, such as I/O routines, mathematical functions, and in some cases types such as integer and char. It is common to consider these to be declared in an extra, invisible, outermost scope, which surrounds the scope in which global objects are declared. The search for bindings described in the previous paragraph terminates at this extra, outermost scope, if it exists, rather than at the scope in which global objects are declared. This outermost scope convention makes it possible for a programmer to define a global object whose name is the same as that of some predefined object (whose “declaration” is thereby hidden, making it invisible).

例 3.5

Example 3.5

嵌套作用域

Nested scopes

图 3.4显示了嵌套作用域的一个示例。4在此示例中,过程P2仅由P1调用,并且不需要在外部可见。因此,它在P1内部声明,将其作用域(可见区域)限制为此处显示的程序部分。类似地,P4仅在P1内可见,P3仅在P2内可见,F1仅在P4内可见。根据嵌套作用域的标准规则,F1可以调用P2P4可以调用F1,但P2不能调用F1

An example of nested scopes appears in Figure 3.4.4 In this example, procedure P2 is called only by P1, and need not be visible outside. It is therefore declared inside P1, limiting its scope (its region of visibility) to the portion of the program shown here. In a similar fashion, P4 is visible only within P1, P3 is visible only within P2, and F1 is visible only within P4. Under the standard rules for nested scopes, F1 could call P2 and P4 could call F1, but P2 could not call F1.

传真:03-04-9780124104099
图 3.4 嵌套子程序示例(以伪代码显示)。竖线表示每个名称的作用域,对于声明在整个子程序中都可见的语言。请注意外部 X 的作用域中的空洞。

尽管嵌套子例程对程序的其余部分是隐藏的,但它们能够访问周围作用域的参数和局部变量(以及其他局部对象)。在我们的示例中,除了A3之外, P3还可以命名(和修改)A1XA2。由于P1F1都声明了名为X 的局部变量,因此内部声明在其作用域的一部分内隐藏了外部声明。F1中对X的使用引用内部X;代码其他区域中对X的使用引用外部X。■

Though they are hidden from the rest of the program, nested subroutines are able to access the parameters and local variables (and other local objects) of the surrounding scope(s). In our example, P3 can name (and modify) A1, X, and A2, in addition to A3. Because P1 and F1 both declare local variables named X, the inner declaration hides the outer one within a portion of its scope. Uses of X in F1 refer to the inner X; uses of X in other regions of the code refer to the outer X. ■

名称到对象的绑定如果被同名的嵌套声明所隐藏,则称其作用域中有漏洞在某些语言中,名称被隐藏的对象在嵌套作用域中根本无法访问(除非它有多个名称)。在其他语言中,程序员可以通过应用限定符作用域解析运算符来访问名称的外部含义。例如,在 Ada 中,名称可以以其声明作用域的名称作为前缀,使用的语法类似于记录中字段的规范。例如, My_proc.X引用子例程My_procX的声明,而不管是否在词汇上更接近的作用域中声明了其他X。在不允许子例程嵌套的 C++ 中, :: X引用X的全局声明,而不管当前子例程是否也具有X。5

A name-to-object binding that is hidden by a nested declaration of the same name is said to have a hole in its scope. In some languages, the object whose name is hidden is simply inaccessible in the nested scope (unless it has more than one name). In others, the programmer can access the outer meaning of a name by applying a qualifier or scope resolution operator. In Ada, for example, a name may be prefixed by the name of the scope in which it is declared, using syntax that resembles the specification of fields in a record. My_proc.X, for example, refers to the declaration of X in subroutine My_proc, regardless of whether some other X has been declared in a lexically closer scope. In C++, which does not allow subroutines to nest, ::X refers to a global declaration of X, regardless of whether the current subroutine also has an X.5

访问非本地对象

Access to Nonlocal Objects

我们已经看到(第 3.2.2 节),编译器可以安排一个帧指针寄存器在运行时指向当前正在执行的子程序的帧。使用此寄存器作为位移(寄存器加偏移量)寻址的基数,目标代码可以访问当前子程序内的对象。但是词汇上围绕子程序中的对象呢?为了找到这些,我们需要一种方法来在运行时找到与这些作用域相对应的帧。由于嵌套子程序可能会调用外部作用域中的例程,因此运行时堆栈帧的顺序不一定与词汇嵌套的顺序相对应。尽管如此,我们可以肯定堆栈中已经存在周围作用域的某个帧,因为除非当前子程序可见,否则不可能调用它,而除非周围作用域处于活动状态,否则它不可能可见。 (在某些语言中,实际上可以保存对嵌套子程序的引用,然后在周围范围不再活跃时调用它。我们将这种可能性推迟到第 3.6.2 节。)

We have already seen (Section 3.2.2) that the compiler can arrange for a frame pointer register to point to the frame of the currently executing subroutine at run time. Using this register as a base for displacement (register plus offset) addressing, target code can access objects within the current subroutine. But what about objects in lexically surrounding subroutines? To find these we need a way to find the frames corresponding to those scopes at run time. Since a nested subroutine may call a routine in an outer scope, the order of stack frames at run time may not necessarily correspond to the order of lexical nesting. Nonetheless, we can be sure that there is some frame for the surrounding scope already in the stack, since the current subroutine could not have been called unless it was visible, and it could not have been visible unless the surrounding scope was active. (It is actually possible in some languages to save a reference to a nested subroutine, and then call it when the surrounding scope is no longer active. We defer this possibility to Section 3.6.2.)

例 3.6

Example 3.6

静态链

Static chains

查找周围范围的框架的最简单方法是在每个框架中维护一个指向“父”框架的静态链接:即词汇上周围的子程序的最近调用的框架。如果子程序在程序的最外层嵌套级别声明,则其框架在运行时将具有空静态链接。如果子程序嵌套深度为k级,则其框架的静态链接以及其父级、祖父级等的静态链接将在运行时形成长度为k的静态链。为了找到在子程序范围向外声明的j个变量或参数,目标代码在运行时可以取消引用静态链j次,然后添加适当的偏移量。静态链如图3.5所示。我们将在第 9.2 节中讨论维护它们所需的代码。■

The simplest way in which to find the frames of surrounding scopes is to maintain a static link in each frame that points to the “parent” frame: the frame of the most recent invocation of the lexically surrounding subroutine. If a subroutine is declared at the outermost nesting level of the program, then its frame will have a null static link at run time. If a subroutine is nested k levels deep, then its frame's static link, and those of its parent, grandparent, and so on, will form a static chain of length k at run time. To find a variable or parameter declared j subroutine scopes outward, target code at run time can dereference the static chain j times, and then add the appropriate offset. Static chains are illustrated in Figure 3.5. We will discuss the code required to maintain them in Section 9.2. ■

传真:03-05-9780124104099
图 3.5 静态链。子程序 A、B、C、D 和 E 嵌套,如左图所示。如果运行时嵌套调用的顺序为 A、E、B、D 和 C,则堆栈中的静态链接将如右图所示。子程序 C 的代码可以找到距帧指针已知偏移量的本地对象。它可以通过取消引用其静态链一次然后应用偏移来找到周围范围 B 的本地对象。它可以通过取消引用其静态链两次然后应用偏移来找到 B 周围范围 A 中的本地对象。

3.3.3 申报顺序

3.3.3 Declaration Order

到目前为止,我们在讨论中忽略了一个重要的细节:假设在块B中的某个地方声明了一个对象x 。 x的作用域是否包括声明之前的B部分?如果是,那么x是否可以在该部分代码中使用?换句话说,表达式E可以引用当前作用域中声明的任何名称,还是只能引用在作用域中在 E 之前声明的名称?

In our discussion so far we have glossed over an important subtlety: suppose an object x is declared somewhere within block B. Does the scope of x include the portion of B before the declaration, and if so can x actually be used in that portion of the code? Put another way, can an expression E refer to any name declared in the current scope, or only to names that are declared before E in the scope?

包括 Algol 60 和 Lisp 在内的几种早期语言都要求所有声明都出现在其范围的开头。人们一开始可能会认为这条规则可以避免上一段中的问题,但事实并非如此,因为声明可以相互引用。6

Several early languages, including Algol 60 and Lisp, required that all declarations appear at the beginning of their scope. One might at first think that this rule would avoid the questions in the preceding paragraph, but it does not, because declarations may refer to one another.6

例 3.7

Example 3.7

使用前声明中的“陷阱”

A”gotcha” in declare-before-use

为了简化编译器的实现,Pascal 修改了要求,规定名称必须在使用前声明。有特殊的机制来适应递归类型和子例程,但一般来说,前向引用(试图在声明之前使用名称)是静态语义错误。但与此同时,Pascal 保留了声明的范围是整个周围块的概念。总而言之,整个块范围和先声明后使用规则可以以令人惊讶的方式相互作用:

In an apparent attempt to simplify the implementation of the compiler, Pascal modified the requirement to say that names must be declared before they are used. There are special mechanisms to accommodate recursive types and subroutines, but in general, a forward reference (an attempt to use a name before its declaration) is a static semantic error. At the same time, however, Pascal retained the notion that the scope of a declaration is the entire surrounding block. Taken together, whole-block scope and declare-before-use rules can interact in surprising ways:

1.常数N=10;

1. const N = 10;

2.

2.

3.程序 foo;

3. procedure foo;

4. const

4. const

5.   M = N;(*静态语义错误!*)

5.   M = N; (* static semantic error! *)

6.  

6.  

7.   N = 20; (* 局部常量声明;隐藏外部 N *)

7.   N = 20; (* local constant declaration; hides the outer N *)

Pascal 表示N的第二个声明涵盖了 foo 的全部,因此语义分析器应该在第 5 行抱怨N在其声明之前被使用。这个错误可能会造成极大的混淆,特别是如果程序员打算使用外部N

Pascal says that the second declaration of N covers all of foo, so the semantic analyzer should complain on line 5 that N is being used before its declaration. The error has the potential to be highly confusing, particularly if the programmer meant to use the outer N:

常量N = 10;

const N = 10;

程序 foo;

procedure foo;

常量

const

M = N;(*静态语义错误!*)

M = N; (* static semantic error! *)

变量

var

A:整数数组[1..M];

A : array [1..M] of integer;

N :真实的;(*隐藏声明*)

N : real; (* hiding declaration *)

这里的一对消息“ N在声明之前使用”和“ N不是常量”几乎肯定没有帮助。

Here the pair of messages “N used before declaration” and “N is not a constant” are almost certainly not helpful.

设计与实现

Design & Implementation

3.3 相互递归

3.3 Mutual recursion

一些 Algol 60 编译器会按程序顺序处理作用域声明。这种策略的不良后果是隐式地禁止了相互递归的子程序和类型,这显然不是语言设计者的本意 [ Atk73 ]。

Some Algol 60 compilers were known to process the declarations of a scope in program order. This strategy had the unfortunate effect of implicitly outlawing mutually recursive subroutines and types, something the language designers clearly did not intend [Atk73].

为了确定任何似乎使用了周围作用域中的名称的声明的有效性,Pascal 编译器必须扫描作用域的其余声明以查看名称是否被隐藏。为了避免这种复杂情况,大多数 Pascal 后继者(以及 Pascal 本身的一些方言)指定标识符的作用域不是声明它的整个块(不包括空洞),而是从声明到结尾的块部分(同样不包括空洞)。例如,如果我们的程序片段是用 Ada 或 C、C++ 或 Java 编写的,则不会报告任何语义错误。M 的声明将引用N的第一个(外部)声明。■

In order to determine the validity of any declaration that appears to use a name from a surrounding scope, a Pascal compiler must scan the remainder of the scope's declarations to see if the name is hidden. To avoid this complication, most Pascal successors (and some dialects of Pascal itself) specify that the scope of an identifier is not the entire block in which it is declared (excluding holes), but rather the portion of that block from the declaration to the end (again excluding holes). If our program fragment had been written in Ada, for example, or in C, C++, or Java, no semantic errors would be reported. The declaration of M would refer to the first (outer) declaration of N. ■

例 3.8

Example 3.8

C# 中的整个块范围

Whole-block scope in C#

C++ 和 Java 进一步放宽了规则,在许多情况下取消了先定义后使用的要求。在这两种语言中,类的成员(包括那些直到程序文本后面才定义的成员)在类的所有方法中都是可见的。在 Java 中,类本身可以按任何顺序声明。有趣的是,虽然 C# 呼应了 Java,要求在使用局部变量(但不要求使用类和成员)之前声明,但它回归了 Pascal 的整个块作用域概念。因此,以下内容在 C# 中是无效的:

C++ and Java further relax the rules by dispensing with the define-before-use requirement in many cases. In both languages, members of a class (including those that are not defined until later in the program text) are visible inside all of the class's methods. In Java, classes themselves can be declared in any order. Interestingly, while C# echos Java in requiring declaration before use for local variables (but not for classes and members), it returns to the Pascal notion of whole-block scope. Thus the following is invalid in C#:

A 类 {

class A {

 常量 int N = 10;

 const int N = 10;

 无效 foo() {

 void foo() {

  const int M = N; // 在声明之前使用内部 N

  const int M = N; // uses inner N before it is declared

  常量 int N = 20;

  const int N = 20;

例 3.9

Example 3.9

Python 中的“本地化”

“Local if written” in Python

从概念的角度来看,声明顺序最简单的方法可能是 Modula-3 方法,它认为声明的作用域是它出现的整个块(减去嵌套声明产生的空洞),声明的顺序无关紧要。对这种方法的主要反对意见是,程序员可能会发现在声明之前使用局部变量是违反直觉的。Python 通过完全省去变量声明,将“整个块”作用域规则更进一步。取而代之的是,它采用了一种不寻常的约定,即子程序S的局部变量正是由S的(静态)主体中的某些语句写入的变量。如果S嵌套在T内,并且名称x出现在ST中赋值语句的左侧,则x是不同的:一个在S中,一个在T中。非局部变量是只读的,除非明确导入(使用 Python 的 global 语句)。我们将在第 14.4.1 节中更详细地考虑这些约定,作为对脚本语言范围的一般讨论的一部分。■

Perhaps the simplest approach to declaration order, from a conceptual point of view, is that of Modula-3, which says that the scope of a declaration is the entire block in which it appears (minus any holes created by nested declarations), and that the order of declarations doesn't matter. The principal objection to this approach is that programmers may find it counterintuitive to use a local variable before it is declared. Python takes the “whole block” scope rule one step further by dispensing with variable declarations altogether. In their place it adopts the unusual convention that the local variables of subroutine S are precisely those variables that are written by some statement in the (static) body of S. If S is nested inside of T, and the name x appears on the left-hand side of assignment statements in both S and T, then the x's are distinct: there is one in S and one in T. Non-local variables are read-only unless explicitly imported (using Python's global statement). We will consider these conventions in more detail in Section 14.4.1, as part of a general discussion of scoping in scripting languages. ■

例 3.10

Example 3.10

Scheme 中的声明顺序

Declaration order in Scheme

为了提高灵活性,现代 Lisp 方言倾向于提供多种声明顺序选项。例如,在 Scheme 中,letreclet * 构造分别使用整个块和声明到块末尾的语义来定义范围。最常用的构造let还提供了另一种选择:

In the interest of flexibility, modern Lisp dialects tend to provide several options for declaration order. In Scheme, for example, the letrec and let* constructs define scopes with, respectively, whole-block and declaration-to-end-of-block semantics. The most frequently used construct, let, provides yet another option:

(let ((A 1)) ;外部范围,A 定义为 1

(let ((A 1)) ; outer scope, with A defined to be 1

 (let ((A 2) ; 内部范围,其中 A 定义为 2

 (let ((A 2) ; inner scope, with A defined to be 2

   (BA));且 B 定义为 A

   (B A)) ; and B defined to be A

  B)) ;返回 B 的值

  B)) ; return the value of B

这里AB的嵌套声明直到声明列表结束之后才生效。因此,当B被定义时, A的重新定义尚未生效。B被定义为外部A,并且整个代码返回 1。■

Here the nested declarations of A and B don't take effect until after the end of the declaration list. Thus when B is defined, the redefinition of A has not yet taken effect. B is defined to be the outer A, and the code as a whole returns 1. ■

声明和定义

Declarations and Definitions

例 3.11

Example 3.11

C 语言中的声明与定义

Declarations vs definitions in C

递归类型和子例程给那些要求在使用名称之前先声明名称的语言带来了一个问题:两个声明怎么能一个在另一个之前出现呢?C 和 C++ 通过区分对象的声明和定义来处理这个问题。声明引入了名称并指明了其范围,但可能会省略某些实现细节。定义对对象进行了足够详细的描述,以便编译器确定其实现。如果声明不够完整,不能成为定义,那么必须在范围的其他地方出现一个单独的定义。在 C 中,我们可以写成

Recursive types and subroutines introduce a problem for languages that require names to be declared before they can be used: how can two declarations each appear before the other? C and C++ handle the problem by distinguishing between the declaration of an object and its definition. A declaration introduces a name and indicates its scope, but may omit certain implementation details. A definition describes the object in sufficient detail for the compiler to determine its implementation. If a declaration is not complete enough to be a definition, then a separate definition must appear somewhere else in the scope. In C we can write

结构管理器;/* 仅声明 */

struct manager; /* declaration only */

结构员工{

struct employee {

  结构经理*老板;

  struct manager *boss;

  结构员工*next_employee;

  struct employee *next_employee;

  

  

};

};

结构管理器{/*定义*/

struct manager { /* definition */

  结构员工*first_employee;

  struct employee *first_employee;

  

  

};

};

and

void list_tail(follow_set fs); /* 仅声明 */

void list_tail(follow_set fs); /* declaration only */

无效列表(follow_set fs)

void list(follow_set fs)

{

{

 切换(输入令牌){

 switch (input_token) {

  案例id:匹配(id);list_tail(fs);

  case id : match(id); list_tail(fs);

 

 

}

}

void list_tail(follow_set fs) /* 定义 */

void list_tail(follow_set fs) /* definition */

{

{

 切换(输入令牌){

 switch (input_token) {

  逗号大小写 : 匹配(逗号); 列表(fs);

  case comma : match(comma); list(fs);

 

 

}

}

manager的初始声明只需要引入一个名称:由于指针通常大小相同,因此编译器无需了解任何manager细节即可确定employee的实现。但是list_tail的初始声明必须包含返回类型和参数列表,因此编译器可以判断list中的调用是正确的。■

The initial declaration of manager needed only to introduce a name: since pointers are generally all the same size, the compiler can determine the implementation of employee without knowing any manager details. The initial declaration of list_tail, however, must include the return type and parameter list, so the compiler can tell that the call in list is correct. ■

嵌套块

Nested Blocks

在许多语言中,包括 Algol 60、C89 和 Ada,局部变量不仅可以在任何子程序的开头声明,还可以在任何begin…end{…})块的顶部声明。其他语言,包括 Algol 68、C 和 C 的所有后代,甚至更加灵活,允许在语句出现的任何位置进行声明。在大多数语言中,嵌套声明会隐藏任何具有相同名称的外部声明(如果外部声明是当前子程序的本地声明,则 Java 和 C# 会将其视为静态语义错误)。

In many languages, including Algol 60, C89, and Ada, local variables can be declared not only at the beginning of any subroutine, but also at the top of any begin… end ({…}) block. Other languages, including Algol 68, C, and all of C's descendants, are even more flexible, allowing declarations wherever a statement may appear. In most languages a nested declaration hides any outer declaration with the same name (Java and C# make it a static semantic error if the outer declaration is local to the current subroutine).

设计与实现

Design & Implementation

3.4 重新申报

3.4 Redeclarations

某些语言,尤其是那些旨在用于交互用途的语言,允许程序员重新声明一个对象:在给定范围内为给定名称创建新的绑定。交互程序员通常使用重新声明来试验替代实现或在早期开发期间修复错误。在大多数交互式语言中,名称的新含义在所有上下文中都会取代旧含义。然而,在 ML 方言中,名称的旧含义可能仍然可由在重新声明名称之前阐述的函数访问。这种设计选择有时可能违反直觉。以下是 OCaml 中的一个例子(以 # 开头的行是用户输入;其他行由解释器打印):

Some languages, particularly those that are intended for interactive use, permit the programmer to redeclare an object: to create a new binding for a given name in a given scope. Interactive programmers commonly use redeclarations to experiment with alternative implementations or to fix bugs during early development. In most interactive languages, the new meaning of the name replaces the old in all contexts. In ML dialects, however, the old meaning of the name may remain accessible to functions that were elaborated before the name was redeclared. This design choice can sometimes be counterintuitive. Here's an example in OCaml (the lines beginning with # are user input; the others are printed by the interpreter):

# 让 x = 1;;

# let x = 1;;

值 x : int = 1

val x : int = 1

# 让 fy = x + y;;

# let f y = x + y;;

val f : int -> int = <fun>

val f : int -> int = <fun>

# 让 x = 2;;

# let x = 2;;

值 x : int = 2

val x : int = 2

#f 3;;

# f 3;;

- :int = 4

- : int = 4

第二行用户输入将f定义为一个参数( y )的函数,该函数返回该参数与之前定义的值x的和。但是,当我们将x重新定义为 2 时,函数不会注意到:它仍然返回y加 1。这种行为反映了 OCaml 通常是即时一点一点编译而不是解释的事实。当x被重新定义时,f已经被编译成一种形式(字节码或机器码),可以直接访问x的旧含义。相比之下,像 Scheme 这样的语言是词法​​作用域的但通常是解释性的,它将名称的绑定存储在已知位置。程序总是通过这些位置间接访问名称的含义:如果名称的含义发生变化,所有对该名称的访问都将使用新的含义。

The second line of user input defines f to be a function of one argument (y) that returns the sum of that argument and the previously defined value x. When we redefine x to be 2, however, the function does not notice: it still returns y plus 1. This behavior reflects the fact that OCaml is usually compiled, bit by bit on the fly, rather than interpreted. When x is redefined, f has already been compiled into a form (bytecode or machine code) that accesses the old meaning of x directly. By comparison, a language like Scheme, which is lexically scoped but usually interpreted, stores the bindings for names in known locations. Programs always access the meanings of names indirectly through those locations: if the meaning of a name changes, all accesses to the name will use the new meaning.

例 3.12

Example 3.12

C 中的内部声明

Inner declarations in C

在嵌套块中声明的变量非常有用,例如在以下 C 代码中:

Variables declared in nested blocks can be very useful, as for example in the following C code:

{

{

int 温度=a;

int temp = a;

a=b;

a = b;

b=温度;

b = temp;

}

}

使temp的声明在词汇上与使用它的代码相邻,使得程序更易于阅读,并且消除了此代码干扰另一个名为temp的变量的可能性。■

Keeping the declaration of temp lexically adjacent to the code that uses it makes the program easier to read, and eliminates any possibility that this code will interfere with another variable named temp. ■

无需运行时工作来为嵌套块中声明的变量分配或释放空间;它们的空间可以包含在子程序序言中分配并在结尾中释放的局部变量的总空间中。练习 3.9考虑如何最小化所需的总空间。

No run-time work is needed to allocate or deallocate space for variables declared in nested blocks; their space can be included in the total space for local variables allocated in the subroutine prologue and deallocated in the epilogue. Exercise 3.9 considers how to minimize the total space required.

03-01-9780124104099检查你的理解

Check Your Understanding

12. 名称到对象绑定的范围是什么意思?

12. What do we mean by the scope of a name-to-object binding?

13. 描述静态和动态作用域之间的区别。

13. Describe the difference between static and dynamic scoping.

14. 什么是精细化

14. What is elaboration?

15. 什么是引用环境

15. What is a referencing environment?

16. 解释最近的嵌套范围规则

16. Explain the closest nested scope rule.

17. 范围解析运算符的用途是什么?

17. What is the purpose of a scope resolution operator?

18. 什么是静态链?它有什么用处?

18. What is a static chain? What is it used for?

19. 什么是前向引用?为什么在许多编程语言中禁止或限制它们?

19. What are forward references? Why are they prohibited or restricted in many programming languages?

20.解释一下 声明定义的区别。为什么这种区别很重要?

20. Explain the difference between a declaration and a definition. Why is the distinction important?

3.3.4 模块

3.3.4 Modules

构建任何大型软件时,一个重要挑战是将工作分摊给程序员,以便工作能够同时在多个方面进行。这种模块化工作主要依赖于信息隐藏的概念,即尽可能使对象和算法对系统中不需要它们的部分不可见。适当模块化的代码通过最小化理解任何给定部分所需的信息量来减少程序员的“认知负荷”。系统。在设计良好的程序中,模块之间的接口尽可能“窄”(即简单),任何可能改变的设计决策都隐藏在单个模块内。

An important challenge in the construction of any large body of software is to divide the effort among programmers in such a way that work can proceed on multiple fronts simultaneously. This modularization of effort depends critically on the notion of information hiding, which makes objects and algorithms invisible, whenever possible, to portions of the system that do not need them. Properly modularized code reduces the “cognitive load” on the programmer by minimizing the amount of information required to understand any given portion of the system. In a well-designed program the interfaces among modules are as “narrow” (i.e., simple) as possible, and any design decision that is likely to change is hidden inside a single module.

信息隐藏对于软件维护(错误修复和增强)至关重要,这往往远远超过大多数商业软件的初始开发成本。除了减少认知负荷外,隐藏还可以降低名称冲突的风险:可见名称越少,新引入的名称与已使用名称相同的可能性就越小。它还保护了数据抽象的完整性:任何试图访问其所属模块之外的对象的行为都会导致编译器发出“未定义符号”错误消息。最后,它有助于区分运行时错误:如果变量具有意外值,我们通常可以确定修改它的代码在变量的范围内。

Information hiding is crucial for software maintenance (bug fixes and enhancement), which tends to significantly outweigh the cost of initial development for most commercial software. In addition to reducing cognitive load, hiding reduces the risk of name conflicts: with fewer visible names, there is less chance that a newly introduced name will be the same as one already in use. It also safeguards the integrity of data abstractions: any attempt to access an object outside of the module to which it belongs will cause the compiler to issue an “undefined symbol” error message. Finally, it helps to compartmentalize run-time errors: if a variable takes on an unexpected value, we can generally be sure that the code that modified it is in the variable's scope.

封装数据和子程序

Encapsulating Data and Subroutines

不幸的是,嵌套子程序提供的信息隐藏仅限于其生命周期与隐藏它们的子程序的生命周期相同的对象。当控制从子程序返回时,其局部变量将不再有效:它们的值将被丢弃。我们已经看到了这个问题的部分解决方案,即 Fortran 中的 save 语句以及 C 和 Algol 中的静态和自身变量。

Unfortunately, the information hiding provided by nested subroutines is limited to objects whose lifetime is the same as that of the subroutine in which they are hidden. When control returns from a subroutine, its local variables will no longer be live: their values will be discarded. We have seen a partial solution to this problem in the form of the save statement in Fortran and the static and own variables of C and Algol.

例 3.13

Example 3.13

伪随机数作为模块的动机

Pseudorandom numbers as a motivation for modules

静态变量允许子程序拥有“内存”——从一次调用到下一次调用保留信息——同时保护该内存不被程序的其他部分意外访问或修改。换句话说,静态变量允许程序员构建单子程序抽象。不幸的是,它们不允许构建接口需要由多个子程序组成的抽象。例如,考虑一个简单的伪随机数生成器。除了主rand_int例程之外,我们可能还需要一个set_seed例程来为特定的伪随机数序列准备生成器(例如,用于确定性测试)。我们希望使生成器的状态(决定下一个伪随机数)对rand_intset_seed都可见,但对程序的其余部分隐藏它。我们可以在许多语言中通过使用模块构造来实现这一目标。■

Static variables allow a subroutine to have “memory”—to retain information from one invocation to the next—while protecting that memory from accidental access or modification by other parts of the program. Put another way, static variables allow programmers to build single-subroutine abstractions. Unfortunately, they do not allow the construction of abstractions whose interface needs to consist of more than one subroutine. Consider, for example, a simple pseudo-random number generator. In addition to the main rand_int routine, we might want a set_seed routine that primes the generator for a specific pseudorandom sequence (e.g., for deterministic testing). We should like to make the state of the generator, which determines the next pseudorandom number, visible to both rand_int and set_seed, but hide it from the rest of the program. We can achieve this goal in many languages through use of a module construct. ■

模块作为抽象

Modules as Abstractions

模块允许将一组对象(子程序、变量、类型等)封装在一起,使得 (1) 模块内部的对象彼此可见,但 (2) 模块内部的对象在外部可能不可见,除非它们被导出以及 (3) 模块外部的对象在内部可能不可见,除非它们被导入。 导入和导出约定在不同语言之间差别很大,但在所有情况下,只有对象的可见性会受到影响;模块不会影响其所包含对象的生命周期。

A module allows a collection of objects—subroutines, variables, types, and so on—to be encapsulated in such a way that (1) objects inside are visible to each other, but (2) objects on the inside may not be visible on the outside unless they are exported, and (3) objects on the outside may not be visible on the inside unless they are imported. Import and export conventions vary significantly from one language to another, but in all cases, only the visibility of objects is affected; modules do not affect the lifetime of the objects they contain.

模块是 20 世纪 70 年代末和 80 年代初的主要语言创新之一;它们出现在 Clu 7(称之为集群)、Modula(1、2 和 3)、Turing 和 Ada 83 等中。以更现代的形式,它们也出现在 Haskell、C++、Java、C# 和所有主要脚本语言中。包括 Ada、Java 和 Perl 在内的几种语言使用术语包不是模块。其他语言,包括 C++、C# 和 PHP,使用命名空间。可以通过使用 C 的单独编译功能在一定程度上模拟模块;我们将在C-3.8 节中讨论这种可能性。

Modules were one of the principal language innovations of the late 1970s and early 1980s; they appeared in Clu7 (which called them clusters), Modula (1, 2, and 3), Turing, and Ada 83, among others. In more modern form, they also appear in Haskell, C++, Java, C#, and all the major scripting languages. Several languages, including Ada, Java, and Perl, use the term package instead of module. Others, including C++, C#, and PHP, use namespace. Modules can be emulated to some degree through use of the separate compilation facilities of C; we discuss this possibility in Section C-3.8.

作为模块使用的一个例子,请考虑图 3.6中所示的伪随机数生成器。如侧边栏 3.5 中所述,此模块(命名空间)通常放在其自己的文件中,然后在 C++ 程序中需要它的地方导入。

As an example of the use of modules, consider the pseudorandom number generator shown in Figure 3.6. As discussed in Sidebar 3.5, this module (namespace) would typically be placed in its own file, and then imported wherever it is needed in a C++ program.

03-06-9780124104099
图 3.6 C++ 中的伪随机数生成器模块。使用线性同余法,默认种子取自当前时间。虽然存在许多更好(更随机)的生成器,但这个生成器很简单,并且可用于许多目的。

例 3.14

Example 3.14

C++ 中的伪随机数生成器

Pseudorandom number generator in C++

在命名空间内进行的名称绑定可能在外部部分或全部隐藏(不活动)——但不会被破坏。在 C++ 中,命名空间只能出现在词汇嵌套的最外层,整数种子将在整个程序执行过程中保留其值,即使它仅对set_seedrand_int可见。

Bindings of names made inside the namespace may be partially or totally hidden (inactive) on the outside—but not destroyed. In C++, where namespaces can appear only at the outermost level of lexical nesting, integer seed would retain its value throughout the execution of the program, even though it is visible only to set_seed and rand_int.

rand_mod命名空间之外,C++ 允许将set_seedrand_int作为rand_mod::set_seedrand_mod::rand_int进行访问。种子变量也可以作为rand_mod::seed进行访问,但这可能不是一个好主意,而且需要rand_mod前缀意味着这种情况不太可能意外发生。

Outside the rand_mod namespace, C++ allows set_seed and rand_int to be accessed as rand_mod::set_seed and rand_mod::rand_int. The seed variable could also be accessed as rand_mod::seed, but this is probably not a good idea, and the need for the rand_mod prefix means it's unlikely to happen by accident.

使用 using 指令,可以根据名称逐个消除前缀的需要:

The need for the prefix can be eliminated, on a name-by-name basis, with a using directive:

使用 rand_mod::rand_int;

using rand_mod::rand_int;

int r = rand_int();

int r = rand_int();

或者,可以立即提供命名空间中声明的完整名称集:

Alternatively, the full set of names declared in a namespace can be made available at once:

使用命名空间 rand_mod;

using namespace rand_mod;

设置种子(12345);

set_seed(12345);

int r = rand_int();

int r = rand_int();

不幸的是,如此全面地暴露模块名称不仅增加了与导入上下文中的名称发生冲突的可能性,而且增加了意外访问逻辑上属于模块私有的对象(如seed )的可能性。

Unfortunately, such wholesale exposure of a module's names increases both the likelihood of conflict with names in the importing context and the likelihood that objects like seed, which are logically private to the module, will be accessed accidentally.

进出口

Imports and Exports

有些语言允许程序员指定从模块导出的名称只能以受限的方式使用。例如,变量可以以只读方式导出,或者类型可以以不透明方式导出,这意味着可以声明该类型的变量,将其作为参数传递给模块的子例程,并可能进行比较或赋值,但不能以任何其他方式操作。

Some languages allow the programmer to specify that names exported from modules be usable only in restricted ways. Variables may be exported read-only, for example, or types may be exported opaquely, meaning that variables of that type may be declared, passed as arguments to the module's subroutines, and possibly compared or assigned to one another, but not manipulated in any other way.

必须明确导入名称的模块被称为封闭作用域。 扩展而言,不需要导入的模块被称为开放作用域。 导入用于记录程序:它们要求模块指定它依赖于程序其余部分的方式,从而提高模块化程度。 它们还通过避免导入任何不需要的内容来减少名称冲突。 Modula(1、2 和 3)和 Haskell 中的模块是封闭的。 C++ 代表了一种越来越常见的选项,其中名称会自动导出,但只有在使用模块名称限定时才可在外部使用- 除非它们被另一个作用域明确“导入”(例如,使用 C++ using 指令),此时它们可以无限定使用。 这个选项,我们可以称之为选择性开放模块,也出现在 Ada、Java、C# 和 Python 等中。

Modules into which names must be explicitly imported are said to be closed scopes. By extension, modules that do not require imports are said to be open scopes. Imports serve to document the program: they increase modularity by requiring a module to specify the ways in which it depends on the rest of the program. They also reduce name conflicts by refraining from importing anything that isn't needed. Modules are closed in Modula (1, 2, and 3) and Haskell. C++ is representative of an increasingly common option, in which names are automatically exported, but are available on the outside only when qualified with the module name—unless they are explicitly “imported” by another scope (e.g., with the C++ using directive), at which point they are available unqualified. This option, which we might call selectively open modules, also appears in Ada, Java, C#, and Python, among others.

模块作为管理器

Modules as Managers

例 3.15

Example 3.15

模块作为类型的“管理器”

Module as “manager” for a type

模块允许将数据设为使用它们的子程序的私有数据,从而促进抽象的构建。但是,当如图 3.6所示使用时,每个模块都定义一个抽象。继续我们前面的例子,有时可能需要有多个伪随机数生成器。例如,在调试游戏时,我们可能希望在某个特定游戏模块(可能是某个特定角色)中获得确定性(可重复)行为,而不管程序其他地方是否使用了伪随机数。如果我们想要有多个生成器,我们可以将命名空间设为生成器类型实例的“管理器” 然后将其从模块中导出,如图3.7所示。管理器习语需要额外的子程序来创建/初始化并可能销毁生成器实例,并且它要求每个子程序(set_seed、rand_int、create)都采用一个额外的参数来指定所讨论的生成器。

Modules facilitate the construction of abstractions by allowing data to be made private to the subroutines that use them. When used as in Figure 3.6, however, each module defines a single abstraction. Continuing our previous example, there are times when it may be desirable to have more than one pseudorandom number generator. When debugging a game, for example, we might want to obtain deterministic (repeatable) behavior in one particular game module (a particular character, perhaps), regardless of uses of pseudorandom numbers elsewhere in the program. If we want to have several generators, we can make our namespace a “manager” for instances of a generator type, which is then exported from the module, as shown in Figure 3.7. The manager idiom requires additional subroutines to create/initialize and possibly destroy generator instances, and it requires that every subroutine (set_seed, rand_int, create) take an extra parameter, to specify the generator in question.

03-07-9780124104099
图 3.7 C++ 中的伪随机数管理模块

鉴于图 3.7中的声明,我们可以创建和使用任意数量的生成器:

Given the declarations in Figure 3.7, we could create and use an arbitrary number of generators:

使用 rand_mgr::generator;

using rand_mgr::generator;

生成器*g1 = rand_mgr::create();

generator *g1 = rand_mgr::create();

生成器*g2 = rand_mgr::create();

generator *g2 = rand_mgr::create();

使用 rand_mgr::rand_int;

using rand_mgr::rand_int;

int r1 = rand_int(g1);

int r1 = rand_int(g1);

int r2 = rand_int(g2);

int r2 = rand_int(g2);

在更复杂的程序中,模块导出几种相关类型可能是有意义的,然后可以将这些类型的实例传递给它的子例程。■

In more complex programs, it may make sense for a module to export several related types, instances of which can then be passed to its subroutines. ■

3.3.5 模块类型和类

3.3.5 Module Types and Classes

例 3.16

Example 3.16

伪随机数生成器类型

A pseudorandom number generator type

Euclid 中出现了多实例问题的替代解决方案,它将每个模块视为一种类型,而不是简单的封装构造。给定一个模块类型,程序员可以声明任意数量的类似模块对象。事实证明,现代面向对象语言的类是模块类型的扩展。访问模块实例通常看起来像访问对象,我们可以在任何面向对象语言中说明这些想法。对于我们的 C++ 伪随机数示例,语法

An alternative solution to the multiple instance problem appeared in Euclid, which treated each module as a type, rather than a simple encapsulation construct. Given a module type, the programmer could declare an arbitrary number of similar module objects. As it turns out, the classes of modern object-oriented languages are an extension of module types. Access to a module instance typically looks like access to an object, and we can illustrate the ideas in any object-oriented language. For our C++ pseudorandom number example, the syntax

生成器*g = rand_mgr::create();

generator *g = rand_mgr::create();

int r = rand_int(g);

int r = rand_int(g);

可能会被取代

might be replaced by

rand_gen *g = new rand_gen();

rand_gen *g = new rand_gen();

int r = g->rand_int();

int r = g->rand_int();

其中rand_gen类的声明如图3.8所示。模块类型或类允许程序员将rand_int例程视为“属于”生成器,而不是必须将生成器作为参数传递给其的单独实体。从概念上讲,每个生成器( rand_gen对象)都有一个专用的rand_int例程。当然,在实践中,创建代码的多个副本会非常浪费。正如我们将在第 10 章中看到的,rand_gen实例实际上共享一对set_seedrand_int例程,并且编译器安排将指向相关实例的指针作为额外的隐藏参数传递给例程。实现结果与图 3.7非常相似,但程序员不必那样想。■

where the rand_gen class is declared as in Figure 3.8. Module types or classes allow the programmer to think of the rand_int routine as “belonging to” the generator, rather than as a separate entity to which the generator must be passed as an argument. Conceptually, there is a dedicated rand_int routine for every generator (rand_gen object). In practice, of course, it would be highly wasteful to create multiple copies of the code. As we shall see in Chapter 10, rand_gen instances really share a single pair of set_seed and rand_int routines, and the compiler arranges for a pointer to the relevant instance to be passed to the routine as an extra, hidden parameter. The implementation turns out to be very similar to that of Figure 3.7, but the programmer need not think of it that way. ■

03-08-9780124104099
图 3.8 C++ 中的伪随机数生成器类

设计与实现

Design & Implementation

3.5 模块与单独编译

3.5 Modules and separate compilation

好的抽象的一个特点是它往往在多种情况下有用。为了便于代码重用,许多语言将模块作为单独编译的基础。Modula-2 实际上提供了两种不同类型的模块:一种(外部模块)用于单独编译,另一种(内部模块)用于更大范围内的文本嵌套。这些选项的经验最终使 Modula-2 的设计者 Niklaus Wirth 得出结论,外部模块是迄今为止最有用的模块;他在后来的语言 Oberon 中省略了内部版本。然而,许多人会争辩说,内部模块只有在通过实例化和继承进行扩展时才能真正发挥作用。事实上,正如本节结尾处所述,许多面向对象语言都提供模块类。前者支持单独编译并用于最大限度地减少名称冲突;后者用于数据抽象。

One of the hallmarks of a good abstraction is that it tends to be useful in multiple contexts. To facilitate code reuse, many languages make modules the basis of separate compilation. Modula-2 actually provided two different kinds of modules: one (external modules) for separate compilation, the other (internal modules) for textual nesting within a larger scope. Experience with these options eventually led Niklaus Wirth, the designer of Modula-2, to conclude that external modules were by far the more useful variety; he omitted the internal version from his subsequent language, Oberon. Many would argue, however, that internal modules find their real utility only when extended with instantiation and inheritance. Indeed, as noted near the end of this section, many object-oriented languages provide both modules and classes. The former support separate compilation and serve to minimize name conflicts; the latter are for data abstraction.

为了便于单独编译,许多语言(其中包括 Modula-2 和 Oberon)的模块可以分为声明部分(header)和实现部分(body),每个部分都占用一个单独的文件。只要存在 header,就可以编译使用给定模块导出的代码;它不依赖于 body。特别是,一旦存在 header,就可以同时进行协作模块 body 的工作。我们将分别在C-3.810.1节中返回单独编译和代码重用的主题。

To facilitate separate compilation, modules in many languages (Modula-2 and Oberon among them) can be divided into a declaration part (header) and an implementation part (body), each of which occupies a separate file. Code that uses the exports of a given module can be compiled as soon as the header exists; it is not dependent on the body. In particular, work on the bodies of cooperating modules can proceed concurrently once the headers exist. We will return to the subjects of separate compilation and code reuse in Sections C-3.8 and 10.1, respectively.

面向对象

Object Orientation

模块类型和类之间的区别在于,后者拥有一对强大的功能,而前者却没有——即继承动态方法分派。8继承允许将新类定义为现有类的扩展或细化。动态方法分派允许细化的类覆盖其父类中操作的定义,并允许在运行时根据特定对象是属于子类还是仅属于父类来选择定义。

The difference between module types and classes is a powerful pair of features found together in the latter but not the former—namely, inheritance and dynamic method dispatch.8 Inheritance allows new classes to be defined as extensions or refinements of existing classes. Dynamic method dispatch allows a refined class to override the definition of an operation in its parent class, and for the choice among definitions to be made at run time, on the basis of whether a particular object belongs to the child class or merely to the parent.

继承促进了一种编程风格,在这种风格中,所有或大多数操作都被视为属于对象,而新对象可以从现有对象继承许多操作,而无需重写代码。类起源于 Simula-67,并在 Smalltalk 中得到进一步发展。它们出现在许多现代语言中,包括 Eiffel、OCaml、C++、Java、C# 和几种脚本语言,尤其是 Python 和 Ruby。继承机制也可以在某些通常不被认为是面向对象的语言中找到,包括 Modula-3、Ada 95 和 Oberon。我们将在第10 章和第 14.4.4 节中研究继承、动态分派及其对作用域规则的影响。

Inheritance facilitates a programming style in which all or most operations are thought of as belonging to objects, and in which new objects can inherit many of their operations from existing objects, without the need to rewrite code. Classes have their roots in Simula-67, and were further developed in Smalltalk. They appear in many modern languages, including Eiffel, OCaml, C++, Java, C#, and several scripting languages, notably Python and Ruby. Inheritance mechanisms can also be found in certain languages that are not usually considered object-oriented, including Modula-3, Ada 95, and Oberon. We will examine inheritance, dynamic dispatch, and their impact on scope rules in Chapter 10 and in Section 14.4.4.

模块类型和类(忽略与继承相关的问题)只需要对第 3.3.4 节中为模块定义的范围规则进行简单的更改。模块类型或类的每个实例A (例如每个rand_gen)都有该模块或类变量的单独副本。执行A的某个操作时,这些变量就可见了。如果将A作为参数传递给某个其他实例B的操作,那么这些变量也可能间接地对其他实例 B 的操作可见。此规则使得在大多数面向对象语言中可以构造二进制(或多元)操作,以操作一个类的多个实例的变量(字段)。

Module types and classes (ignoring issues related to inheritance) require only simple changes to the scope rules defined for modules in Section 3.3.4. Every instance A of a module type or class (e.g., every rand_gen) has a separate copy of the module or class's variables. These variables are then visible when executing one of A's operations. They may also be indirectly visible to the operations of some other instance B if A is passed as a parameter to one of those operations. This rule makes it possible in most object-oriented languages to construct binary (or more-ary) operations that can manipulate the variables (fields) of more than one instance of a class.

包含类的模块

Modules Containing Classes

例 3.17

Example 3.17

大型应用程序中的模块和类

Modules and classes in a large application

虽然从模块到模块类型再到类有一个明显的发展过程,但类不一定在所有情况下都能充分替代模块。假设我们正在开发一款交互式“第一人称”游戏。类层次结构可能正是我们所需要的,用来表示角色、财产、建筑物、目标和大量其他数据抽象。同时,尤其是在拥有大量程序员的项目中,我们可能希望将游戏的功能划分为大型子系统,如图形和渲染、物理和策略。这些子系统实际上不是数据抽象,我们可能不希望创建它们的多个实例。它们自然地被传统模块捕获,特别是如果这些模块是为单独编译而设计的(第 3.8 节)。认识到多实例抽象和功能细分的必要性,许多语言(包括 C++、Java、C#、Python 和 Ruby)都提供了单独的类和模块机制。■

While there is a clear progression from modules to module types to classes, it is not necessarily the case that classes are an adequate replacement for modules in all cases. Suppose we are developing an interactive “first person” game. Class hierarchies may be just what we need to represent characters, possessions, buildings, goals, and a host of other data abstractions. At the same time, especially on a project with a large team of programmers, we will probably want to divide the functionality of the game into large-scale subsystems such as graphics and rendering, physics, and strategy. These subsystems are really not data abstractions, and we probably don't want the option to create multiple instances of them. They are naturally captured with traditional modules, particularly if those modules are designed for separate compilation (Section 3.8). Recognizing the need for both multi-instance abstractions and functional subdivision, many languages, including C++, Java, C#, Python, and Ruby, provide separate class and module mechanisms. ■

3.3.6 动态作用域

3.3.6 Dynamic Scoping

在具有动态作用域的语言中,名称和对象之间的绑定取决于运行时的控制流,特别是子例程的调用顺序。与上一节讨论的静态作用域规则相比,动态作用域规则通常非常简单:给定名称的“当前”绑定是执行期间最近遇到的绑定,并且尚未通过从其作用域返回而被破坏。

In a language with dynamic scoping, the bindings between names and objects depend on the flow of control at run time, and in particular on the order in which subroutines are called. In comparison to the static scope rules discussed in the previous section, dynamic scope rules are generally quite simple: the “current” binding for a given name is the one encountered most recently during execution, and not yet destroyed by returning from its scope.

具有动态作用域的语言包括 APL、Snobol、Tcl、T E X(本书所使用的排版语言)以及 Lisp 的早期方言[ MAE + 65Moo78TM81 ] 和 Perl。9因为控制流通常无法提前预测,所以具有动态作用域的语言中名称和对象之间的绑定通常无法由编译器确定因此,具有动态作用域的语言中的许多语义规则成为动态语义而不是静态语义的问题。例如,表达式中的类型检查和子程序调用中的参数检查通常必须推迟到运行时。为了进行所有这些检查,具有动态作用域的语言倾向于被解释而不是被编译。

Languages with dynamic scoping include APL, Snobol, Tcl, TEX (the typesetting language with which this book was created), and early dialects of Lisp [MAE+65, Moo78, TM81] and Perl.9 Because the flow of control cannot in general be predicted in advance, the bindings between names and objects in a language with dynamic scoping cannot in general be determined by a compiler. As a result, many semantic rules in a language with dynamic scoping become a matter of dynamic semantics rather than static semantics. Type checking in expressions and argument checking in subroutine calls, for example, must in general be deferred until run time. To accommodate all these checks, languages with dynamic scoping tend to be interpreted, rather than compiled.

例 3.18

Example 3.18

静态与动态作用域

Static vs dynamic scoping

考虑图 3.9中的程序。如果静态作用域有效,则该程序打印 1。如果动态作用域有效,则输出取决于运行时在第 8 行读取的值:如果输入为正,则程序打印 2;否则打印 1。为什么会有这种差异?问题在于第 3 行对变量 n 的赋值是指第 1 行声明的全局变量还是第 5 行声明的局部变量。静态作用域规则要求引用解析为最近的词法封闭声明,即全局n。过程首先将n更改为 1,然后第 12 行打印该值。另一方面,动态作用域规则要求我们在运行时为n选择最新的活动绑定。

Consider the program in Figure 3.9. If static scoping is in effect, this program prints a 1. If dynamic scoping is in effect, the output depends on the value read at line 8 at run time: if the input is positive, the program prints a 2; otherwise it prints a 1. Why the difference? At issue is whether the assignment to the variable n at line 3 refers to the global variable declared at line 1 or to the local variable declared at line 5. Static scope rules require that the reference resolve to the closest lexically enclosing declaration, namely the global n. Procedure first changes n to 1, and line 12 prints this value. Dynamic scope rules, on the other hand, require that we choose the most recent, active binding for n at run time.

03-09-9780124104099
图 3.9 静态作用域与动态作用域。程序输出既取决于作用域规则,在动态作用域的情况下,还取决于运行时读取的值。

设计与实现

Design & Implementation

3.6 动态作用域

3.6 Dynamic scoping

目前还不完全清楚 Lisp 和其他早期解释型语言中动态作用域的使用是故意的还是无意的。认为它可能是故意的的一个原因是,它使解释器可以非常轻松地查找名称的含义:所需的只是一个声明堆栈(我们将在 C-3.4.2 节中更仔细地检查这个堆栈)。不幸的是,这种简单的实现具有非常高的运行时成本,并且经验表明动态作用域使程序更难理解。现代共识似乎是动态作用域通常是一个坏主意(请参阅练习 3.17探索 3.36了解两个例外)。

It is not entirely clear whether the use of dynamic scoping in Lisp and other early interpreted languages was deliberate or accidental. One reason to think that it may have been deliberate is that it makes it very easy for an interpreter to look up the meaning of a name: all that is required is a stack of declarations (we examine this stack more closely in Section C-3.4.2). Unfortunately, this simple implementation has a very high run-time cost, and experience indicates that dynamic scoping makes programs harder to understand. The modern consensus seems to be that dynamic scoping is usually a bad idea (see Exercise 3.17 and Exploration 3.36 for two exceptions).

当我们进入主程序时,我们为n创建一个绑定。当我们进入过程second时,我们会创建另一个绑定。当我们执行第 3 行的赋值语句时,我们引用的n将取决于我们是通过second进入 first还是直接从主程序进入。如果我们通过second进入,我们将把值 1 赋给second的本地n。如果我们从主程序进入,我们将把值 1 赋给全局n。在任一情况下,第 12 行的写入都将引用全局n,因为当控制权返回主程序时,second的本地n及其绑定将被销毁。■

We create a binding for n when we enter the main program. We create another when and if we enter procedure second. When we execute the assignment statement at line 3, the n to which we are referring will depend on whether we entered first through second or directly from the main program. If we entered through second, we will assign the value 1 to second's local n. If we entered from the main program, we will assign the value 1 to the global n. In either case, the write at line 12 will refer to the global n, since second's local n will be destroyed, along with its binding, when control returns to the main program. ■

例 3.19

Example 3.19

具有动态作用域的运行时错误

Run-time errors with dynamic scoping

使用动态作用域,与引用环境相关的错误可能直到运行时才会被发现。例如,在图 3.10中,过程 foo 中局部变量max_score的声明意外地重新定义了函数scaled_score使用的全局变量,然后从foo调用该全局变量。由于全局max_score是一个整数,而局部max_score是一个浮点数,因此至少在某些语言中,动态语义检查将在运行时导致类型冲突消息。如果局部max_score是一个整数,则不会检测到任何错误,但程序几乎肯定会产生不正确的结果。这种错误很难发现。■

With dynamic scoping, errors associated with the referencing environment may not be detected until run time. In Figure 3.10, for example, the declaration of local variable max_score in procedure foo accidentally redefines a global variable used by function scaled_score, which is then called from foo. Since the global max_score is an integer, while the local max_score is a floating-point number, dynamic semantic checks in at least some languages will result in a type clash message at run time. If the local max_score had been an integer, no error would have been detected, but the program would almost certainly have produced incorrect results. This sort of error can be very hard to find. ■

传真:03-10-9780124104099
图 3.10 动态作用域的问题。当动态作用域规则允许过程 foo 改变 max_score 的含义时,过程 scaled_score 可能不会按照程序员的意图执行。

3.4 实现范围

3.4 Implementing Scope

为了跟踪静态作用域程序中的名称,编译器依赖于称为符号表的数据抽象。本质上,符号表是一本字典:它将名称映射到编译器所知道的信息。最基本的操作是插入新的映射(名称到对象的绑定)或查找给定名称的已有信息。静态作用域规则允许给定的名称对应于程序不同部分的不同对象(从而对应于不同的信息),这增加了复杂性。大多数静态作用域的变体都可以通过用enter_scopeleave_scope操作扩充基本字典式符号表来处理,以跟踪可见性。表中的任何内容都不会被删除;整个结构在整个编译过程中都保留,然后保存以供调试器或运行时反射(类型查找)机制使用。

To keep track of the names in a statically scoped program, a compiler relies on a data abstraction called a symbol table. In essence, the symbol table is a dictionary: it maps names to the information the compiler knows about them. The most basic operations are to insert a new mapping (a name-to-object binding) or to look up the information that is already present for a given name. Static scope rules add complexity by allowing a given name to correspond to different objects—and thus to different information—in different parts of the program. Most variations on static scoping can be handled by augmenting a basic dictionary-style symbol table with enter_scope and leave_scope operations to keep track of visibility. Nothing is ever deleted from the table; the entire structure is retained throughout compilation, and then saved for use by debuggers or run-time reflection (type lookup) mechanisms.

在具有动态作用域的语言中,解释器(或编译器的输出)必须在运行时执行类似于符号表插入和查找的操作。原则上,编译器中用于符号表的任何组织都可用于跟踪解释器中的名称到对象的绑定,反之亦然。实际上,动态作用域的实现倾向于采用两种特定组织之一:关联列表中央引用表

In a language with dynamic scoping, an interpreter (or the output of a compiler) must perform operations analogous to symbol table insert and lookup at run time. In principle, any organization used for a symbol table in a compiler could be used to track name-to-object bindings in an interpreter, and vice versa. In practice, implementations of dynamic scoping tend to adopt one of two specific organizations: an association list or a central reference table.

03-02-9780124104099 更深入地

IN MORE DEPTH

具有可视性支持的符号表可以用几种不同的方式实现。配套站点上介绍了一种由 LeBlanc 和 Cook [ CL83 ] 提出的有吸引力的方法,以及关联列表和中央引用表。

A symbol table with visibility support can be implemented in several different ways. One appealing approach, due to LeBlanc and Cook [CL83], is described on the companion site, along with both association lists and central reference tables.

关联列表(简称A 列表)只是名称/值对的列表。当用于实现动态作用域时,它的功能相当于一个堆栈:遇到新声明时将其推送,并在其出现的作用域末尾弹出。通过从顶部向下搜索列表来找到绑定。中央引用表通过维护从名称到其当前含义的显式映射来避免线性时间搜索的需要。查找速度更快,但作用域的进入和退出稍微复杂一些,并且保存引用环境以供将来使用变得更加困难(我们将在第3.6.1 节中进一步讨论这个问题)。

An association list (or A-list for short) is simply a list of name/value pairs. When used to implement dynamic scoping it functions as a stack: new declarations are pushed as they are encountered, and popped at the end of the scope in which they appeared. Bindings are found by searching down the list from the top. A central reference table avoids the need for linear-time search by maintaining an explicit mapping from names to their current meanings. Lookup is faster, but scope entry and exit are somewhat more complex, and it becomes substantially more difficult to save a referencing environment for future use (we discuss this issue further in Section 3.6.1).

3.5 范围内名称的含义

3.5 The Meaning of Names within a Scope

到目前为止,在我们对命名和范围的讨论中,我们假设在程序中任何给定点,名称和可见对象之间存在一对一映射。事实并非如此。在程序中的同一点引用同一对象的两个或多个名称被称为别名。在程序中的给定点可以引用多个对象的名称被称为重载。重载又与更一般的多态性主题相关,多态性允许子例程或其他程序片段根据其参数的类型以不同的方式运行。

So far in our discussion of naming and scopes we have assumed that there is a one-to-one mapping between names and visible objects at any given point in a program. This need not be the case. Two or more names that refer to the same object at the same point in the program are said to be aliases. A name that can refer to more than one object at a given point in the program is said to be overloaded. Overloading is in turn related to the more general subject of polymorphism, which allows a subroutine or other program fragment to behave in different ways depending on the types of its arguments.

3.5.1 别名

3.5.1 Aliases

别名的简单示例出现在许多编程语言的变体记录和联合中(我们将在C-8.1.3 节中详细讨论这些功能)。

Simple examples of aliases occur in the variant records and unions of many programming languages (we will discuss these features detail in Section C-8.1.3).

例 3.20

Example 3.20

使用参数进行别名

Aliasing with parameters

它们也自然地出现在使用基于指针的数据结构的程序中。在许多语言中创建别名的更巧妙的方法是将变量通过引用传递给也直接访问该变量的子例程。考虑以下 C++ 代码:

They also arise naturally in programs that make use ofpointer-based data structures. A more subtle way to create aliases in many languages is to pass a variable by reference to a subroutine that also accesses that variable directly. Consider the following code in C++:

双精度和,平方和;

double sum, sum_of_squares;

void gather(double& x) { // x 通过引用传递

void accumulate(double& x) { // x is passed by reference

 总和 += x;

 sum += x;

 平方和+=x*x;

 sum_of_squares += x * x;

}

}

如果我们将sum作为参数传递给accumpl,那么sumx将在被调用例程中互为别名,程序可能不会按照程序员的意图运行。■

If we pass sum as an argument to accumulate, then sum and x will be aliases for one another inside the called routine, and the program will probably not do what the programmer intended. ■

例 3.21

Example 3.21

别名和代码改进

Aliases and code improvement

一般来说,别名往往会使程序比本来更令人困惑。它们还使编译器更难执行某些重要的代码改进。请考虑以下 C 代码:

As a general rule, aliases tend to make programs more confusing than they otherwise would be. They also make it much more difficult for a compiler to perform certain important code improvements. Consider the following C code:

int a,b,*p,*q;

int a, b, *p, *q;

a = *p; /* 从 p 引用的变量读取 */

a = *p; /* read from the variable referred to by p */

*q = 3; /* 分配给 q 引用的变量 */

*q = 3; /* assign to the variable referred to by q */

b = *p; /* 从 p 引用的变量读取 */

b = *p; /* read from the variable referred to by p */

设计与实现

Design & Implementation

3.7 C 和 Fortran 中的指针

3.7 Pointers in C and Fortran

指针倾向于引入别名是为什么从历史上看,Fortran 编译器倾向于比 C 编译器生成更快的代码的原因之一:指针在 C 中被大量使用,但在 Fortran 77 及其前身中却没有出现。只是在最近几年,复杂的别名分析算法才使得 C 编译器在生成代码的速度上能够与 Fortran 编译器相媲美。指针分析非常重要,因此 C99 标准的设计者决定在该语言中增加一个新关键字。restrict限定符附加到指针声明时,表示程序员断言指针指向的对象在当前范围内没有别名。确保断言正确是程序员的责任;编译器无需尝试检查它。C99 还引入了严格别名。这允许编译器假定不同类型的指针永远不会引用内存中的同一位置。大多数编译器都提供了命令行选项来禁用利用此规则的优化;否则,(编写不佳的)遗留程序在更高的优化级别进行编译时可能会出现不正确的行为。

The tendency of pointers to introduce aliases is one of the reasons why Fortran compilers tended, historically, to produce faster code than C compilers: pointers are heavily used in C, but missing from Fortran 77 and its predecessors. It is only in recent years that sophisticated alias analysis algorithms have allowed C compilers to rival their Fortran counterparts in speed of generated code. Pointer analysis is sufficiently important that the designers of the C99 standard decided to add a new keyword to the language. The restrict qualifier, when attached to a pointer declaration, is an assertion on the part of the programmer that the object to which the pointer refers has no alias in the current scope. It is the programmer's responsibility to ensure that the assertion is correct; the compiler need not attempt to check it. C99 also introduced strict aliasing. This allows the compiler to assume that pointers of different types will never refer to the same location in memory. Most compilers provide a command-line option to disable optimizations that exploit this rule; otherwise (poorly written) legacy programs may behave incorrectly when compiled at higher optimization levels.

在大多数机器上,对 a 的初始赋值将要求将 * p加载到寄存器中。由于访问内存的代价很高,编译器将希望保留已加载的值并在对b的赋值中重用它。但是,除非它可以验证pq不能引用同一个对象(即 * p和 * q不是别名),否则它将无法做到这一点。虽然这种编译时验证在许多常见情况下是可能的,但一般来说是无法判定的。■

The initial assignment to a will, on most machines, require that *p be loaded into a register. Since accessing memory is expensive, the compiler will want to hang on to the loaded value and reuse it in the assignment to b. It will be unable to do so, however, unless it can verify that p and q cannot refer to the same object—that is, that *p and *q are not aliases. While compile-time verification of this sort is possible in many common cases, in general it's undecidable. ■

3.5.2 重载

3.5.2 Overloading

例 3.22

Example 3.22

Ada 中的重载枚举常量

Overloaded enumeration constants in Ada

大多数编程语言都至少提供了一种有限的重载形式。例如,在 C 语言中,加号 (+) 用于命名几个不同的函数,包括有符号和无符号整数以及浮点加法。大多数程序员并不担心这两个函数之间的区别——毕竟它们都基于相同的数学概念——但它们采用不同类型的参数并对底层位执行非常不同的操作。Ada 的枚举常量中出现了一种稍微复杂一些的重载形式。在图 3.11中,常量octdec指的是月份或数字基数,具体取决于它们出现的上下文。■

Most programming languages provide at least a limited form of overloading. In C, for example, the plus sign (+) is used to name several different functions, including signed and unsigned integer and floating-point addition. Most programmers don't worry about the distinction between these two functions—both are based on the same mathematical concept, after all—but they take arguments of different types and perform very different operations on the underlying bits. A slightly more sophisticated form of overloading appears in the enumeration constants of Ada. In Figure 3.11, the constants oct and dec refer either to months or to numeric bases, depending on the context in which they appear. ■

传真:03-11-9780124104099
图 3.11 Ada 中的枚举常量的重载。

例 3.23

Example 3.23

解决歧义重载

Resolving ambiguous overloads

在编译器的符号表中,必须通过安排查找例程返回所请求名称的可能含义列表来处理(解决)重载。然后,语义分析器必须根据上下文从列表元素中进行选择。当上下文不足以做出决定时(如图3.11中的 print 调用所示),语义分析器必须宣布错误。大多数允许重载枚举常量的语言都允许程序员明确提供适当的上下文。例如,在 Ada 中,可以这样说

Within the symbol table of a compiler, overloading must be handled (resolved) by arranging for the lookup routine to return a list of possible meanings for the requested name. The semantic analyzer must then choose from among the elements of the list based on context. When the context is not sufficient to decide, as in the call to print in Figure 3.11, then the semantic analyzer must announce an error. Most languages that allow overloaded enumeration constants allow the programmer to provide appropriate context explicitly. In Ada, for example, one can say

打印(月份'(十月));

print(month'(oct));

在 Modula-3 和 C# 中,每次使用枚举常量时都必须以类型名称作为前缀,即使不会产生歧义:

In Modula-3 and C#, every use of an enumeration constant must be prefixed with a type name, even when there is no chance of ambiguity:

mo := month.dec; (* Modula-3 *)

mo := month.dec; (* Modula-3 *)

pb = print_base.oct; //C#

pb = print_base.oct; // C#

在 C 中,枚举常量根本无法重载;给定范围内可见的每个常量必须是不同的。C++11 引入了新语法,让程序员可以控制此行为:枚举常量必须是不同的;枚举类常量必须用类名限定(例如,month::oct)。■

In C, one cannot overload enumeration constants at all; every constant visible in a given scope must be distinct. C++11 introduced new syntax to give the programmer control over this behavior: enum constants must be distinct; enum class constants must be qualified with the class name (e.g., month::oct). ■

例 3.24

Example 3.24

C++ 中的重载

Overloading in C++

Ada 和 C++ 都具有用于重载子程序名称的复杂功能。许多 C++ 功能都可移植到 Java 和 C# 中。给定的名称可以引用同一范围内的任意数量的子程序,只要这些子程序的参数数量或类型不同即可。C++ 示例如图3.12所示。■

Both Ada and C++ have elaborate facilities for overloading subroutine names. Many of the C++ facilities carry over to Java and C#. A given name may refer to an arbitrary number of subroutines in the same scope, so long as the subroutines differ in the number or types of their arguments. C++ examples appear in Figure 3.12. ■

传真:03-12-9780124104099
图 3.12 C++ 中重载的简单示例。在每种情况下,编译器都可以根据参数的数量和类型判断要使用哪个函数。

重新定义内置运算符

Redefining Built-in Operators

例 3.25

Example 3.25

Ada 中的运算符重载

Operator overloading in Ada

许多语言还允许使用用户定义函数重载内置算术运算符(+*等)。Ada、C++ 和 C# 通过定义每个运算符的替代前缀形式,并将常用中缀形式定义为前缀形式的缩写(或“语法糖”)来实现这一点。在 Ada 中,A + B是“+”(A, B)的缩写。如果“ + ”(前缀形式)被重载,则+ (中缀形式)也适用于新类型。必须能够从AB的类型解析重载(确定哪个+是预期的)。■

Many languages also allow the built-in arithmetic operators (+, , *, etc.) to be overloaded with user-defined functions. Ada, C++, and C# do this by defining alternative prefix forms of each operator, and defining the usual infix forms to be abbreviations (or “syntactic sugar”) for the prefix forms. In Ada, A + B is short for “+”(A, B). If “+“ (the prefix form) is overloaded, then + (the infix form) will work for the new types as well. It must be possible to resolve the overloading (determine which + is intended) from the types of A and B. ■

例 3.26

Example 3.26

C++ 中的运算符重载

Operator overloading in C++

Fortran 90 提供了一个特殊的接口构造,可用于将运算符与某个命名的二进制函数关联起来。在面向对象的 C++ 和 C# 中,A + B可能是operator+(A, B)A.operator+(B)的缩写。在后一种情况下,A是定义operator+函数的类(模块类型)的实例。在 C++ 中,有人可能会说

Fortran 90 provides a special interface construct that can be used to associate an operator with some named binary function. In C++ and C#, which are object-oriented, A + B maybe short for either operator+(A, B) or A.operator+(B). In the latter case, A is an instance of a class (module type) that defines an operator+ function. In C++ one might say

类复合体 {

class complex {

 双实数,虚数;

 double real, imaginary;

 

 

民众:

public:

 复杂运算符 + (复杂其他) {

 complex operator+(complex other) {

  返回复数(实数 + other.real,虚数 + other.imaginary);

  return complex(real + other.real, imaginary + other.imaginary);

 }

 }

 

 

};

};

复合物A,B,C;

complex A, B, C;

C = A + B; // 使用用户定义的运算符 +

C = A + B; // uses user-defined operator+

C# 语法类似。■

C# syntax is similar. ■

例 3.27

Example 3.27

Haskell 中的中缀运算符

Infix operators in Haskell

在 Haskell 中,用户定义的中缀运算符只是名称由非字母数字字符组成的函数:

In Haskell, user-defined infix operators are simply functions whose names consist of non-alphanumeric characters:

让 a @@ b = a * 2 + b

let a @@ b = a * 2 + b

这里我们定义了一个名为@@的 2 参数运算符。我们也可以使用通常的前缀表示法来声明它,在这种情况下我们需要将名称括在括号中:

Here we have defined a 2-argument operator named @@. We could also have declared it with the usual prefix notation, in which case we would have needed to enclose the name in parentheses:

让(@@)ab=a * 2 + b

let (@@) a b=a * 2 + b

无论哪种方式,3 @@ 4(@@) 3 4都将计算为10。 (任意函数也可以通过将其名称括在反引号中用作 Haskell 中的中缀运算符。 如果定义得当,gcd 8 128 'gcd' 12都将计算为4。)

Either way, both 3 @@ 4 and (@@) 3 4 will evaluate to 10. (An arbitrary function can also be used as infix operator in Haskell by enclosing its name in backquotes. With an appropriate definition, gcd 8 12 and 8 'gcd' 12 will both evaluate to 4.)

与大多数语言不同,Haskell 允许程序员指定用户定义运算符的结合性和优先级。我们将在6.1.1 节中回顾这个主题。■

Unlike most languages, Haskell allows the programmer to specify both the associativity and the precedence of user-defined operators. We will return to this subject in Section 6.1.1. ■

例 3.28

Example 3.28

使用类型类进行重载

Overloading with type classes

在 Haskell 中,运算符和普通函数都可以使用称为类型类的机制进行重载。其中最简单的一个是 Eq 类,它在标准库中声明为

Both operators and ordinary functions can be overloaded in Haskell, using a mechanism known as type classes. Among the simplest of these is the class Eq, declared in the standard library as

设计与实现

Design & Implementation

3.8 OCaml 中的用户定义运算符

3.8 User-defined operators in OCaml

OCaml 不支持重载,但它允许用户创建新的运算符,其名称(与 Haskell 中一样)由非字母数字字符组成。每个这样的名称都必须以内置运算符之一的名称开头,新运算符从中继承其语法角色(前缀、中缀或后缀)和优先级。因此,例如,+ . 用于浮点加法;+/ 用于“bignum”(任意精度)整数加法。

OCaml does not support overloading, but it does allow the user to create new operators, whose names—as in Haskell—consist of non-alphanumeric characters. Each such name must begin with the name of one of the built-in operators, from which the new operator inherits its syntactic role (prefix, infix, or postfix) and precedence. So, for example, + . is used for floating-point addition; +/ is used for “bignum” (arbitrary precision) integer addition.

类 Eq a,其中

class Eq a, where

(==)::a->a->布尔值

(==) :: a -> a -> Bool

此声明将Eq确立为提供==运算符的类型集。对于某些特定类型a ,任何==实例都必须采用两个参数(每个参数都是类型a)并返回布尔结果。换句话说,==是一个重载运算符,所有类型的Eq 类都支持该运算符;每个此类类型都必须提供自己的相等定义。整数的定义同样来自标准库,如下所示:

This declaration establishes Eq as the set of types that provide an == operator. Any instance of ==, for some particular type a, must take two arguments (each of type a) and return a Boolean result. In other words, == is an overloaded operator, supported by all types of class Eq; each such type must provide its own equality definition. The definition for integers, again from the standard library, looks like this:

实例 Eq Integer

instance Eq Integer where

x == y = x'integerEq' y

x == y = x 'integerEq' y

此处,integerEq是内置的、非重载的整数相等运算符。■

Here integerEq is the built-in, non-overloaded integer equality operator. ■

类型类可以基于自身构建。例如,Haskell 0rd类包含所有支持运算符<><=>=的Eq类型。Num 类(略微简化)包含所有支持加法、减法和乘法的Eq类型。除了使重载比大多数语言更明确之外,类型类还可以指定某些多态函数只能在其参数属于支持某些特定重载函数的类型时使用(有关此主题的更多信息,请参阅边栏 7.7)。

Type classes can build upon themselves. The Haskell 0rd class, for example, encompasses all Eq types that also support the operators <, >, <=, and >=. The Num class (simplifying a bit) encompasses all Eq types that also support addition, subtraction, and multiplication. In addition to making overloading a bit more explicit than it is in most languages, type classes make it possible to specify that certain polymorphic functions can be used only when their arguments are of a type that supports some particular overloaded function (for more on this subject, see Sidebar 7.7).

相关概念

Related Concepts

在考虑函数和子程序调用时,区分重载与强制和多态的相关概念非常重要。在某些情况下,这三种方法都可用于将多种类型的参数传递给看似单个命名的程序(或从中返回多种类型的值)。然而,语法相似性隐藏了语义和语用方面的显著差异。

When considering function and subroutine calls, it is important to distinguish overloading from the related concepts of coercion and polymorphism. All three can be used, in certain circumstances, to pass arguments of multiple types to (or return values of multiple types from) what appears to be a single named routine. The syntactic similarity, however, hides significant differences in semantics and pragmatics.

强制转换(我们将在7.2.2 节中详细介绍)是指当周围上下文需要第二种类型时,编译器会自动将一种类型的值转换为另一种类型的值的过程。多态性(我们将在7.1.2 、7.310.1.114.4.4中讨论)允许单个子程序接受多种类型的参数。

Coercion, which we will cover in more detail in Section 7.2.2, is the process by which a compiler automatically converts a value of one type into a value of another type when that second type is required by the surrounding context. Polymorphism, which we will consider in Sections 7.1.2,7.3,10.1.1, and 14.4.4, allows a single subroutine to accept arguments of multiple types.

例 3.29

Example 3.29

打印多种类型的对象

Printing objects of multiple types

考虑一个旨在将其参数显示在标准输出流上的打印例程,并假设我们希望能够显示多种类型的对象。通过重载,我们可以为每种感兴趣的类型编写一个单独的打印例程。然后,当它看到对print(my_object)的调用时,编译器会根据my_object的类型选择适当的例程。

Consider a print routine designed to display its argument on the standard output stream, and suppose that we wish to be able to display objects of multiple types. With overloading, we might write a separate print routine for each type of interest. Then when it sees a call to print(my_object), the compiler would choose the appropriate routine based on the type of my_object.

现在假设我们已经有一个接受浮点参数的打印例程。通过强制转换,我们可以通过将整数传递给这个现有例程来打印整数,而不必编写一个新例程。当编译器看到对print(my_integer)的调用时,它会在调用之前自动将参数强制(转换)为浮点类型。

Now suppose we already have a print routine that accepts a floating-point argument. With coercion, we might be able to print integers by passing them to this existing routine, rather than writing a new one. When it sees a call to print(my_integer), the compiler would coerce (convert) the argument automatically to floating-point type prior to the call.

最后,假设我们有一种语言,其中许多类型都支持to_string操作,该操作将生成该类型对象的字符串表示。然后我们可能能够编写一个多态打印例程,该例程接受为其定义to_string的任何类型的参数。to_string操作本身可能是多态的、内置的或只是重载的;在任何这些情况下,print 都可以调用它并输出结果。■

Finally, suppose we have a language in which many types support a to_string operation that will generate a character-string representation of an object of that type. We might then be able to write a polymorphic print routine that accepts an argument of any type for which to_string is defined. The to_string operation might itself be polymorphic, built in, or simply overloaded; in any of these cases, print could call it and output the result. ■

简而言之,重载允许程序员为多个对象赋予相同的名称,并根据上下文(对于子例程,则根据参数的数量或类型)消除歧义(解析)。强制允许编译器执行自动类型转换,使参数符合某些现有例程的预期类型。多态性允许单个例程接受多种类型的参数,前提是它仅尝试以其类型支持的方式使用它们。

In short, overloading allows the programmer to give the same name to multiple objects, and to disambiguate (resolve) them based on context—for subroutines, on the number or types of arguments. Coercion allows the compiler to perform an automatic type conversion to make an argument conform to the expected type of some existing routine. Polymorphism allows a single routine to accept arguments of multiple types, provided that it attempts to use them only in ways that their types support.

03-01-9780124104099检查你的理解

Check Your Understanding

21. 解释信息隐藏的重要性。

21. Explain the importance of information hiding.

22. 什么是不透明出口?

22. What is an opaque export?

23. 为什么区分模块的头部主体是有用的?

23. Why might it be useful to distinguish between the header and the body of a module?

24. 范围关闭是什么意思

24. What does it mean for a scope to be closed?

25. 解释“模块作为管理器”和“模块作为类型”之间的区别。

25. Explain the distinction between “modules as managers” and “modules as types.”

26. 课程与模块有何不同?

26. How do classes differ from modules?

27. 为什么使用同一种语言的模块和类会很有用?

27. Why might it be useful to have modules and classes in the same language?

28. 为什么使用动态作用域意味着需要进行运行时类型检查?

28. Why does the use of dynamic scoping imply the need for run-time type checking?

29. 解释编译器符号表的用途。

29. Explain the purpose of a compiler's symbol table.

30. 什么是别名?为什么它们在语言设计和实现中被视为问题?

30. What are aliases? Why are they considered a problem in language design and implementation?

31.解释 C 语言中restrict限定符的值。

31. Explain the value of the restrict qualifier in C.

32. 什么是重载?它与强制转换多态有何不同?

32. What is overloading? How does it differ from coercion and polymorphism?

33. Haskell 中的 类型类是什么?它们有什么用途?

33. What are type classes in Haskell? What purpose do they serve?

3.6 引用环境的绑定

3.6 The Binding of Referencing Environments

我们已经在3.3 节中看到了范围规则如何确定程序中给定语句的引用环境。静态范围规则指定引用环境取决于声明名称的程序块的词汇嵌套。动态范围规则指定引用环境取决于运行时遇到声明的顺序。我们尚未考虑的另一个问题出现在允许创建对子例程的引用(例如,通过将其作为参数传递)的语言中。何时应将范围规则应用于此类子例程:首次创建引用时,还是最终调用例程时?答案对于具有动态范围的语言尤其重要,尽管我们将看到即使在具有静态范围的语言中,答案也很重要。

We have seen in Section 3.3 how scope rules determine the referencing environment of a given statement in a program. Static scope rules specify that the referencing environment depends on the lexical nesting of program blocks in which names are declared. Dynamic scope rules specify that the referencing environment depends on the order in which declarations are encountered at run time. An additional issue that we have not yet considered arises in languages that allow one to create a reference to a subroutine—for example, by passing it as a parameter. When should scope rules be applied to such a subroutine: when the reference is first created, or when the routine is finally called? The answer is particularly important for languages with dynamic scoping, though we shall see that it matters even in languages with static scoping.

例 3.30

Example 3.30

深浅绑定

Deep and shallow binding

图 3.13中以伪代码的形式显示了一个动态作用域示例。过程print_selected_records被认为是一个通用例程,它知道如何遍历数据库中的记录,而不管这些记录代表的是人、链轮还是沙拉。它以数据库、用于做出打印/不打印决策的谓词以及知道如何格式化此特定数据库的记录中的数据的子例程作为参数。我们假设过程print_person使用非局部变量line_length的值来计算其输出中的列数和列宽。在具有动态作用域的语言中,过程print_selected_records很自然地会在本地声明和初始化此变量,并且知道print_routine中的代码会在需要时获取它。要使这种编码技术发挥作用,在print_selected_records实际调用该例程之前,不得创建print_routine 的引用环境。这种对作为参数传递的子程序的引用环境进行后期绑定的做法称为浅绑定。在具有动态作用域的语言中,它通常是默认的。

A dynamic scoping example appears as pseudocode in Figure 3.13. Procedure print_selected_records is assumed to be a general-purpose routine that knows how to traverse the records in a database, regardless of whether they represent people, sprockets, or salads. It takes as parameters a database, a predicate to make print/don't print decisions, and a subroutine that knows how to format the data in the records of this particular database. We have hypothesized that procedure print_person uses the value of nonlocal variable line_length to calculate the number and width of columns in its output. In a language with dynamic scoping, it is natural for procedure print_selected_records to declare and initialize this variable locally, knowing that code inside print_routine will pick it up if needed. For this coding technique to work, the referencing environment of print_routine must not be created until the routine is actually called by print_selected_records. This late binding of the referencing environment of a subroutine that has been passed as a parameter is known as shallow binding. It is usually the default in languages with dynamic scoping.

传真:03-13-9780124104099
图 3.13 程序(伪代码)说明绑定规则的重要性。有人可能会认为深度绑定适用于函数 older_than_threshold 的环境(用于访问阈值),而浅绑定适用于过程 print_person 的环境(用于访问 line_length)。

相比之下,对于函数older_than_threshold ,浅绑定可能效果不佳。例如,如果过程print_selected_records恰好有一个名为 Threshold 的局部变量,那么主程序设置的用于影响older_than_threshold行为的变量在最终调用该函数时将不可见,并且谓词不太可能正常工作。在这种情况下,最初将函数作为参数传递的代码会考虑特定的引用环境(当前环境);它不希望在任何其他环境中调用该例程。因此,在例程首次作为参数传递时绑定环境,然后在最终调用该例程时恢复该环境是有意义的。也就是说,我们安排older_than_threshold在最终被调用时看到与在创建引用时调用它时看到的相同的引用环境。这种对引用环境的早期绑定称为深度绑定。在具有静态作用域的语言中它几乎总是默认的,并且有时也可作为动态作用域的选项使用。■

For function older_than_threshold, by contrast, shallow binding may not work well. If, for example, procedure print_selected_records happens to have a local variable named threshold, then the variable set by the main program to influence the behavior of older_than_threshold will not be visible when the function is finally called, and the predicate will be unlikely to work correctly. In such a situation, the code that originally passes the function as a parameter has a particular referencing environment (the current one) in mind; it does not want the routine to be called in any other environment. It therefore makes sense to bind the environment at the time the routine is first passed as a parameter, and then restore that environment when the routine is finally called. That is, we arrange for older_than_threshold to see, when it is eventually called, the same referencing environment it would have seen if it had been called at the point where the reference was created. This early binding of the referencing environment is known as deep binding. It is almost always the default in languages with static scoping, and is sometimes available as an option with dynamic scoping as well. ■

“语用学” — 2015/11/2 — 19:18 — 第 153 页 — #183

“pragmatics” — 2015/11/2 — 19:18 — page 153 — #183

3.6.1 子程序闭包

3.6.1 Subroutine Closures

深度绑定是通过创建引用环境(通常是当前调用子程序时执行的环境)的显式表示并将其与对子程序的引用捆绑在一起来实现的。整个捆绑包称为闭包。通常,子程序本身可以在闭包中通过指向其代码的指针来表示。在具有动态作用域的语言中,引用环境的表示取决于语言实现是使用关联列表还是中央引用表来进行运行时查找名称;我们将在 C-3.4.2 节末尾考虑这些替代方案。

Deep binding is implemented by creating an explicit representation of a referencing environment (generally the one in which the subroutine would execute if called at the present time) and bundling it together with a reference to the subroutine. The bundle as a whole is referred to as a closure. Usually the subroutine itself can be represented in the closure by a pointer to its code. In a language with dynamic scoping, the representation of the referencing environment depends on whether the language implementation uses an association list or a central reference table for run-time lookup of names; we consider these alternatives at the end of Section C-3.4.2.

在使用动态作用域的早期 Lisp 方言中,深度绑定可通过内置的原始函数获得,该函数将函数作为参数并返回一个闭包,该闭包的引用环境是函数在当时调用时将执行的环境。然后可以将闭包作为参数传递给另一个函数。如果最终调用它,它将在保存的环境中执行。(闭包的工作方式与大多数 Lisp 方言中的“裸”函数略有不同:必须通过将它们传递给内置原始函数funcallapply 来调用它们。)

In early dialects of Lisp, which used dynamic scoping, deep binding was available via the built-in primitive function, which took a function as its argument and returned a closure whose referencing environment was the one in which the function would have executed if called at that moment in time. The closure could then be passed as a parameter to another function. If and when it was eventually called, it would execute in the saved environment. (Closures work slightly differently from “bare” functions in most Lisp dialects: they must be called by passing them to the built-in primitives funcall or apply.)

乍一看,人们可能会认为在具有静态作用域的语言中,引用环境的绑定时间并不重要。毕竟,静态作用域名称的含义取决于其词汇嵌套,而不是执行流程,并且无论是在将子程序作为参数传递时捕获,还是在调用子程序时捕获,这种嵌套都是相同的。问题在于,正在运行的程序可能具有在递归子程序中声明的对象的多个实例。具有静态作用域的语言中的闭包在创建闭包时捕获每个对象的当前实例。当调用闭包的子程序时,它将找到这些捕获的实例,即使随后通过递归调用创建了较新的实例。

At first glance, one might be tempted to think that the binding time of referencing environments would not matter in a language with static scoping. After all, the meaning of a statically scoped name depends on its lexical nesting, not on the flow of execution, and this nesting is the same whether it is captured at the time a subroutine is passed as a parameter or at the time the subroutine is called. The catch is that a running program may have more than one instance of an object that is declared within a recursive subroutine. A closure in a language with static scoping captures the current instance of every object, at the time the closure is created. When the closure's subroutine is called, it will find these captured instances, even if newer instances have subsequently been created by recursive calls.

例 3.31

Example 3.31

具有静态作用域的绑定规则

Binding rules with static scoping

可以设想将静态作用域与浅绑定相结合 [ VF82 ],但这种组合似乎没有多大意义,而且似乎没有被任何语言采用。图 3.14包含一个 Python 程序,该程序说明了在存在静态作用域的情况下绑定规则的影响。此程序打印 1。使用浅绑定,它将打印 2。■

One could imagine combining static scoping with shallow binding [VF82], but the combination does not seem to make much sense, and does not appear to have been adopted in any language. Figure 3.14 contains a Python program that illustrates the impact of binding rules in the presence of static scoping. This program prints a 1. With shallow binding it would print a 2. ■

传真:03-14-9780124104099
图 3.14 Python 中的深度绑定。右侧是运行时堆栈的概念视图。闭包中捕获的引用环境显示为虚线框和箭头。当通过形式参数P调用B时,存在两个I实例。由于P的闭包是在 A 的初始调用中创建的,因此 B的静态链接(实线箭头)指向该先前调用的框架。B打印语句中使用该调用的I实例,输出1。

应该注意的是,绑定规则仅在访问既非本地也非全局、但在某个中间嵌套级别定义的对象时才与静态作用域有关。如果对象是当前执行的子例程的本地对象,那么子例程是直接调用还是通过闭包调用并不重要;无论是哪种情况,在子例程开始运行时都会创建本地对象。如果对象是全局的,则永远不会有多个实例,因为程序的主体不是递归的。因此,绑定规则与 C 等语言无关,因为 C 语言没有嵌套的子例程,或者 Modula-2 只允许将最外层的子例程作为参数传递,从而确保在子例程之外定义的任何变量都是全局的。(绑定规则与 PL/I 和 Ada 83 等语言无关,因为这些语言根本不允许将子例程作为参数传递。)

It should be noted that binding rules matter with static scoping only when accessing objects that are neither local nor global, but are defined at some intermediate level of nesting. If an object is local to the currently executing subroutine, then it does not matter whether the subroutine was called directly or through a closure; in either case local objects will have been created when the subroutine started running. If an object is global, there will never be more than one instance, since the main body of the program is not recursive. Binding rules are therefore irrelevant in languages like C, which has no nested subroutines, or Modula-2, which allows only outermost subroutines to be passed as parameters, thus ensuring that any variable defined outside the subroutine is global. (Binding rules are also irrelevant in languages like PL/I and Ada 83, which do not permit subroutines to be passed as parameters at all.)

那么假设我们有一种具有静态作用域的语言,其中嵌套的子例程可以作为参数传递,并具有深度绑定。为了表示子例程S的闭包,我们可以简单地将指向S代码的指针与静态链接一起保存,如果现在在当前环境中调用该链接, S将使用它。当最终调用S时,我们会暂时恢复保存的静态链接,而不是创建一个新的链接。当S沿着其静态链访问非本地对象时,它将找到创建闭包时当前的对象实例。这个实例可能没有创建闭包时的,但它的身份至少会反映闭包创建者的意图。

Suppose then that we have a language with static scoping in which nested subroutines can be passed as parameters, with deep binding. To represent a closure for subroutine S, we can simply save a pointer to S's code together with the static link that S would use if it were called right now, in the current environment. When S is finally called, we temporarily restore the saved static link, rather than creating a new one. When S follows its static chain to access a nonlocal object, it will find the object instance that was current at the time the closure was created. This instance may not have the value it had at the time the closure was created, but its identity, at least, will reflect the intent of the closure's creator.

3.6.2 第一类价值和无限范围

3.6.2 First-Class Values and Unlimited Extent

一般来说,如果编程语言中的值可以作为参数传递、从子程序返回或赋值给变量,则称该值具有一等地位。在大多数编程语言中,整数和字符等简单类型都是一等值。相反,第二类值可以作为参数传递,但不能从子程序返回或赋值给变量,第三类值甚至不能作为参数传递。正如我们将在第9.3.2 节中看到的那样,标签(在具有标签的语言中)通常是第三类值,但在 Algol 中它们是第二类值。子程序表现出最多的变化。它们是所有函数式编程语言和大多数脚本语言中的第一类值。它们在 C# 中也是一等值,并且在其他几种命令式语言中(有一些限制)也是一等值,包括 Fortran、Modula-2 和 -3、Ada 95、C 和 C++。10在大多数其他命令式语言中,它们是二等值,而在 Ada 83 中,它们是三等值。

In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable. Simple types such as integers and characters are first-class values in most programming languages. By contrast, a “second-class” value can be passed as a parameter, but not returned from a subroutine or assigned into a variable, and a “third-class” value cannot even be passed as a parameter. As we shall see in Section 9.3.2, labels (in languages that have them) are usually third-class values, but they are second-class values in Algol. Subroutines display the most variation. They are first-class values in all functional programming languages and most scripting languages. They are also first-class values in C# and, with some restrictions, in several other imperative languages, including Fortran, Modula-2 and -3, Ada 95, C, and C++.10 They are second-class values in most other imperative languages, and third-class values in Ada 83.

例 3.32

Example 3.32

在 Scheme 中返回一等子程序

Returning a first-class subroutine in Scheme

到目前为止,我们对绑定的讨论仅考虑了二等子程序。在具有嵌套作用域的语言中,一等子程序引入了额外的复杂性:它们增加了对子程序的引用可能超出了声明该例程的作用域的执行时间。考虑以下 Scheme 中的示例:

Our discussion of binding so far has considered only second-class subroutines. First-class subroutines in a language with nested scopes introduce an additional level of complexity: they raise the possibility that a reference to a subroutine may outlive the execution of the scope in which that routine was declared. Consider the following example in Scheme:

1. (定义 plus-x

1. (define plus-x

2.   (λ(x)

2.   (lambda (x)

3.    (lambda (y) (+ xy))))

3.    (lambda (y) (+ x y))))

4.

4.

5. (设 ((f (加-x 2)))

5. (let ((f (plus-x 2)))

6.   (f 3)) ;返回 5

6.   (f 3))      ; returns 5

这里第 5 行的let结构声明了一个新函数f ,它是使用参数2调用plus-x的结果。函数plus-x定义在第 1 行。它返回在第 3 行声明的(未命名)函数。但该函数引用plus-x的参数x。当在第 6 行调用f时,它的引用环境将包括plus-x中的x,尽管plus-x已经返回(参见图 3.15)。我们必须以某种方式确保x仍然可用。■

Here the let construct on line 5 declares a new function, f, which is the result of calling plus-x with argument 2. Function plus-x is defined at line 1. It returns the (unnamed) function declared at line 3. But that function refers to parameter x of plus-x. When f is called at line 6, its referencing environment will include the x in plus-x, despite the fact that plus-x has already returned (see Figure 3.15). Somehow we must ensure that x remains available. ■

传真:03-15-9780124104099
图 3.15 需要无限延伸。在示例 3.32中调用函数plus-x时,它返回(图左侧)一个包含匿名函数的闭包。该函数的引用环境包含plus-xmain — 包括plus-x本身的局部变量。当随后调用匿名函数时(图右侧),它必须能够访问闭包环境中的变量 — 特别是plus-x内的x — 尽管plus-x不再处于活动状态。

如果在每个作用域执行结束时销毁本地对象(并回收其空间),那么在长寿命闭包中捕获的引用环境可能会充满悬垂引用。 为了避免这个问题,大多数函数式语言指定本地对象具有无限范围:它们的生命周期无限期地持续下去。 只有当垃圾收集系统能够证明它们永远不会再使用时,才能回收它们的空间。大多数命令式语言中的本地对象(除自身/静态变量外)具有有限的范围:它们在其作用域执行结束时被销毁。 (C# 和 Smalltalk 是该规则的例外,大多数脚本语言也是如此。)可以在堆栈上分配具有有限范围的本地对象的空间。 通常必须在堆上分配具有无限范围的本地对象的空间。

If local objects were destroyed (and their space reclaimed) at the end of each scope's execution, then the referencing environment captured in a long-lived closure might become full of dangling references. To avoid this problem, most functional languages specify that local objects have unlimited extent: their lifetimes continue indefinitely. Their space can be reclaimed only when the garbage collection system is able to prove that they will never be used again. Local objects (other than own/static variables) in most imperative languages have limited extent: they are destroyed at the end of their scope's execution. (C# and Smalltalk are exceptions to the rule, as are most scripting languages.) Space for local objects with limited extent can be allocated on a stack. Space for local objects with unlimited extent must generally be allocated on a heap.

由于希望保持子程序局部变量的基于堆栈的分配,具有一等子程序的命令式语言通常必须采用替代机制来避免闭包的悬空引用问题。当然,C 和(Fortran 90 之前的)Fortran 没有嵌套子程序。Modula-2 只允许对最外层子程序创建引用(最外层例程是一等值;嵌套例程是三等值)。Modula-3 允许将嵌套子程序作为参数传递,但只能返回或将最外层例程存储在变量中(最外层例程是一等值;嵌套例程是二等值)。Ada 95 允许返回嵌套例程,但前提是声明嵌套例程的范围等于或大于声明的返回类型的范围。该包含规则虽然比严格必要的规则更为保守(它禁止图 3.14的 Ada 等效规则),但却无法将子程序引用传播到程序中该程序的引用环境未处于活动状态的部分。

Given the desire to maintain stack-based allocation for the local variables of subroutines, imperative languages with first-class subroutines must generally adopt alternative mechanisms to avoid the dangling reference problem for closures. C and (pre-Fortran 90) Fortran, of course, do not have nested subroutines. Modula-2 allows references to be created only to outermost subroutines (outermost routines are first-class values; nested routines are third-class values). Modula-3 allows nested subroutines to be passed as parameters, but only outermost routines to be returned or stored in variables (outermost routines are first-class values; nested routines are second-class values). Ada 95 allows a nested routine to be returned, but only if the scope in which it was declared is the same as, or larger than, the scope of the declared return type. This containment rule, while more conservative than strictly necessary (it forbids the Ada equivalent of Figure 3.14), makes it impossible to propagate a subroutine reference to a portion of the program in which the routine's referencing environment is not active.

设计与实现

Design & Implementation

3.9 约束规则及范围

3.9 Binding rules and extent

绑定机制和范围概念与实现问题密切相关。A 列表使构建闭包变得容易(第 C-3.4.2 节),但 C 的非嵌套子例程和 Modula-2 中禁止将非全局子例程作为参数传递的规则也是如此。类似地,许多命令式语言缺乏一等子例程,这在很大程度上反映了避免堆分配的愿望,这对于具有无限范围的局部变量来说是必需的。

Binding mechanisms and the notion of extent are closely tied to implementation issues. A-lists make it easy to build closures (Section C-3.4.2), but so do the non-nested subroutines of C and the rule against passing nonglobal subroutines as parameters in Modula-2. In a similar vein, the lack of first-class subroutines in many imperative languages reflects in large part the desire to avoid heap allocation, which would be needed for local variables with unlimited extent.

3.6.3 对象闭包

3.6.3 Object Closures

例 3.33

Example 3.33

Java 中的对象闭包

An object closure in Java

如3.6.1 节所述,仅当传递嵌套子程序时,闭包中的引用环境才会变得重要。这意味着在没有嵌套子程序的语言中,一等子程序的实现是微不足道的。同时,这意味着使用这种语言的程序员缺少一个有用的特性:传递带有上下文的子程序的能力。在面向对象语言中,还有另一种方法可以实现类似的效果:我们可以将子程序封装为一个简单对象的方法,并让该对象的字段保存该方法的上下文。在 Java 中,我们可以编写示例 3.32的等效代码,如下所示:

As noted in Section 3.6.1, the referencing environment in a closure will be nontrivial only when passing a nested subroutine. This means that the implementation of first-class subroutines is trivial in a language without nested subroutines. At the same time, it means that a programmer working in such a language is missing a useful feature: the ability to pass a subroutine with context. In object-oriented languages, there is an alternative way to achieve a similar effect: we can encapsulate our subroutine as a method of a simple object, and let the object's fields hold context for the method. In Java we might write the equivalent of Example 3.32 as follows:

接口 IntFunc {

interface IntFunc {

 公共 int 调用(int i);

 public int call(int i);

}

}

类 PlusX 实现 IntFunc {

class PlusX implements IntFunc {

 最终 int x;

 final int x;

 PlusX(int n) { x = n; }

 PlusX(int n) { x = n; }

 公共 int 调用(int i){返回 i + x; }

 public int call(int i) { return i + x; }

}

}

IntFunc f = new PlusX(2);

IntFunc f = new PlusX(2);

System.out.println(f.call(3)); // 打印 5

System.out.println(f.call(3)); // prints 5

此处,接口 IntFunc定义了一个静态类型,用于封装从整数到整数的函数。类PlusX是此类型的具体实现,可以为任何整数常量x实例化。示例 3.32中的 Scheme 代码在(plus-x 2)返回的子例程闭包中捕获x,而此处的 Java 代码在new PlusX(2)返回的对象闭包中捕获x。■

Here the interface IntFunc defines a static type for objects enclosing a function from integers to integers. Class PlusX is a concrete implementation of this type, and can be instantiated for any integer constant x. Where the Scheme code in Example 3.32 captured x in the subroutine closure returned by (plus-x 2), the Java code here captures x in the object closure returned by new PlusX(2). ■

例 3.34

Example 3.34

C# 中的委托

Delegates in C#

扮演函数角色的对象及其引用环境可能被称为对象闭包函数对象函子。(这与 Prolog、ML 或 Haskell 中函子的使用无关。)在 C# 中,一等子程序是委托类型的实例:

An object that plays the role of a function and its referencing environment may variously be called an object closure, a function object, or a functor. (This is unrelated to use of the term functor in Prolog, ML, or Haskell.) In C#, a first-class subroutine is an instance of a delegate type:

委托 int IntFunc(int i);

delegate int IntFunc(int i);

此类型可以为任何与指定参数和返回类型匹配的子程序实例化。该子程序可能是静态的,也可能是某个对象的方法:

This type can be instantiated for any subroutine that matches the specified argument and return types. That subroutine may be static, or it may be a method of some object:

静态 int Plus2(int i) 返回 i + 2;

static int Plus2(int i) return i + 2;

IntFunc f = new IntFunc(Plus2);

IntFunc f = new IntFunc(Plus2);

Console.WriteLine(f(3)); // 打印 5

Console.WriteLine(f(3)); // prints 5

PlusX 类

class PlusX

 int x;

 int x;

 公共 PlusX(int n) x = n;

 public PlusX(int n) x = n;

 公共 int 调用(int i)返回 i + x;

 public int call(int i) return i + x;

IntFunc g = new IntFunc(new PlusX(2).call);

IntFunc g = new IntFunc(new PlusX(2).call);

Console.WriteLine(g(3)); // 打印 5

Console.WriteLine(g(3)); // prints 5

例 3.35

Example 3.35

代表和无限范围

Delegates and unlimited extent

值得注意的是,尽管 C# 在一般情况下不允许子例程嵌套,但它允许从匿名(未命名)方法内联实例化委托。这使我们能够模仿示例 3.32中的代码:

Remarkably, though C# does not permit subroutines to nest in the general case, it does allow delegates to be instantiated in-line from anonymous (unnamed) methods. These allow us to mimic the code of Example 3.32:

静态IntFunc PlusY(int y){

static IntFunc PlusY(int y) {

返回委托(int i){返回i + y; };

return delegate(int i) { return i + y; };

}

}

IntFunc h = PlusY(2);

IntFunc h = PlusY(2);

这里y 的范围是无限的!编译器安排在堆中分配它,并通过包含在闭包中的隐藏指针间接引用它。此实现仅在需要时才产生动态存储分配(以及最终的垃圾收集)的成本;在常见情况下,局部变量保留在堆栈中。■

Here y has unlimited extent! The compiler arranges to allocate it in the heap, and to refer to it indirectly through a hidden pointer, included in the closure. This implementation incurs the cost of dynamic storage allocation (and eventual garbage collection) only when it is needed; local variables remain in the stack in the common case. ■

例 3.36

Example 3.36

C++ 中的函数对象

Function objects in C++

对象闭包非常重要,有些语言用特殊语法支持它们。在 C++ 中,重写 operator() 的类的对象可以像函数一样被调用:

Object closures are sufficiently important that some languages support them with special syntax. In C++, an object of a class that overrides operator() can be called as if it were a function:

类 int_func {

class int_func {

民众:

public:

 虚拟 int 运算符()(int i)= 0;

 virtual int operator()(int i) = 0;

};

};

类 plus_x : 公共 int_func {

class plus_x : public int_func {

 常量 int x;

 const int x;

民众:

public:

 plus_x (int n) : x (n) { }

 plus_x(int n) : x(n) { }

 虚拟 int 运算符()(int i){ 返回 i + x; }

 virtual int operator()(int i) { return i + x; }

};

};

plus_x f(2);

plus_x f(2);

cout << f(3) << “\n”; // 打印 5

cout << f(3) << “\n”; // prints 5

对象f还可以传递给任何需要类int_func参数的函数。■

Object f could also be passed to any function that expected a parameter of class int_func. ■

3.6.4 Lambda 表达式

3.6.4 Lambda Expressions

例 3.37

Example 3.37

C# 中的 lambda 表达式

A lambda expression in C#

到目前为止,在我们大多数的例子中,闭包都对应于以通常方式声明和命名的子例程。然而,在示例 3.32的 Scheme 代码中,我们看到了一个匿名函数——lambda表达式。同样,在示例 3.35中,我们看到了 C# 中的匿名委托。使用 C# 的 lambda 语法可以使该示例变得更简单:

In most of our examples so far, closures have corresponded to subroutines that were declared—and named—in the usual way. In the Scheme code of Example 3.32, however, we saw an anonymous function—a lambda expression. Similarly, in Example 3.35, we saw an anonymous delegate in C#. That example can be made even simpler using C#'s lambda syntax:

静态IntFunc PlusY(int y){

static IntFunc PlusY(int y) {

返回 i => i + y;

return i => i + y;

}

}

这里,示例 3.35中的关键字 delegate已被 => 符号替换,该符号将匿名函数的参数列表(在本例中为i)与其函数体(表达式i + y)分隔开。在具有多个参数的函数中,参数列表将被括在括号中;在更长、更复杂的函数中,函数体可以是代码块,带有一个或多个显式返回语句。■

Here the keyword delegate of Example 3.35 has been replaced by an => sign that separates the anonymous function's parameter list (in this case, just i) from its body (the expression i + y). In a function with more than one parameter, the parameter list would be parenthesized; in a longer, more complicated function, the body could be a code block, with one or more explicit return statements. ■

例 3.38

Example 3.38

多种 lambda 语法

Variety of lambda syntax

“lambda 表达式”这一术语来自lambda 演算,这是函数式编程的一种形式化符号,我们将在第 11 章中详细讨论它。正如我们所料,lambda 语法在不同语言之间差别很大:

The term “lambda expression” comes from the lambda calculus, a formal notation for functional programming that we will consider in more detail in Chapter 11. As one might expect, lambda syntax varies quite a bit from one language to another:

(λ(ij)(> ij)ij); 方案
(int i, int j) => i > j ? i : j// C#
fun ij -> 如果 i > j 则 i 否则 j(* OCaml *)
->(i,j){ i > j ? i : j }# 红宝石

每个表达式的结果都是两个参数中较大的一个。

Each of these expressions evaluates to the larger of two parameters.

在 Scheme 和 OCaml 这两种以函数式语言为主的语言中,lambda 表达式只是一个函数,可以像任何其他函数一样被调用:

In Scheme and OCaml, which are predominately functional languages, a lambda expression simply is a function, and can be called in the same way as any other function:

; 方案:
((lambda (ij) (> ij) ij) 5 8);计算结果为 8
(* OCaml:*)
(fun ij -> 如果 i > j 则 i 否则 j) 5 8(* 同样地 *)

在以命令式为主的 Ruby 中,必须明确调用 lambda 表达式:

In Ruby, which is predominately imperative, a lambda expression must be called explicitly:

打印 ->(i,j){ i > j ? i : j }.call(5,8)

print ->(i, j){ i > j ? i : j }.call(5, 8)

在 C# 中,必须先将表达式赋值给变量(或传递到参数)然后才能调用它:

In C#, the expression must be assigned into a variable (or passed into a parameter) before it can be invoked:

Func<int, int, int> m= (i, j) =>i >j ? i : j;

Func<int, int, int> m= (i, j) =>i >j ? i : j;

控制台.WriteLine(m.Invoke(5, 8));

Console.WriteLine(m.Invoke(5, 8));

这里,Func<int, int, int>是一个函数类型的命名,该函数接受两个整数参数并返回一个整数结果。■

Here Func<int, int, int> is how one names the type of a function taking two integer parameters and returning an integer result. ■

在函数式编程语言中,lambda 表达式使将函数作为值进行操作变得容易——以各种方式组合它们以动态创建新函数。这种操作在命令式语言中不太常见,但即使在命令式语言中,lambda 表达式也可以帮助促进代码重用和通用性。一个特别常见的习惯用法是回调——一个传递到库中的子例程,允许库在适当的时候“回调”到主程序中。回调的示例包括传递到排序例程中的比较运算符、用于过滤集合元素的谓词或响应某些未来事件而调用的处理程序(参见第 9.6.2 节)。

In functional programming languages, lambda expressions make it easy to manipulate functions as values—to combine them in various ways to create new functions on the fly. This sort of manipulation is less common in imperative languages, but even there, lambda expressions can help encourage code reuse and generality. One particularly common idiom is the callback—a subroutine, passed into a library, that allows the library to “call back” into the main program when appropriate. Examples of callbacks include a comparison operator passed into a sorting routine, a predicate used to filter elements of a collection, or a handler to be called in response to some future event (see Section 9.6.2).

随着一等子程序的日益流行,lambda 表达式甚至进入了 C++,而 C++ 缺乏垃圾收集功能,并且强调基于堆栈的分配,因此解决变量捕获问题特别困难。所采用的解决方案符合语言的本质,更强调效率和表现力,而不是运行时安全性。

With the increasing popularity of first-class subroutines, lambda expressions have even made their way into C++, where the lack of garbage collection and the emphasis on stack-based allocation make it particularly difficult to solve the problem of variable capture. The adopted solution, in keeping with the nature of the language, stresses efficiency and expressiveness more than run-time safety.

例 3.39

Example 3.39

C++11 中的简单 lambda 表达式

A simple lambda expression in C++11

在简单情况下,不需要捕获非局部变量。如果V是整数向量,则以下内容将打印小于 50 的所有元素:

In the simple case, no capture of nonlocal variables is required. If V is a vector of integers, the following will print all elements less than 50:

for_each(V.begin(),V.end(),

for_each(V.begin(), V.end(),

 [](int e){ if (e < 50) cout << e << “ “; }

 [](int e){ if (e < 50) cout << e << “ “; }

(英文):

);

这里for_each是一个标准库例程,它将第三个参数(一个函数)应用于前两个参数指定范围内的集合的每个元素参数。在我们的示例中,函数由 lambda 表达式表示,由空方括号引入。编译器将 lambda 表达式转换为匿名函数,然后通过 C++ 的常用机制(指向代码的简单指针)将其传递给for_each。■

Here for_each is a standard library routine that applies its third parameter—a function—to every element of a collection in the range specified by its first two parameters. In our example, the function is denoted by a lambda expression, introduced by the empty square brackets. The compiler turns the lambda expression into an anonymous function, which is then passed to for_each via C++'s usual mechanism—a simple pointer to the code. ■

例 3.40

Example 3.40

C++ lambda 表达式中的变量捕获

Variable capture in C++ lambda expressions

但是,假设我们想要打印所有小于k 的元素,其中k是 lambda 表达式范围之外的变量。现在我们在 C++ 中有两个选项:

Suppose, however, that we wanted to print all elements less than k, where k is a variable outside the scope of the lambda expression. We now have two options in C++:

[=](int e){ if (e < k) cout << e << “ “; }

[=](int e){ if (e < k) cout << e << “ “; }

[&](int e){ if (e < k) cout << e << “ “; }

[&](int e){ if (e < k) cout << e << “ “; }

这两种方式都会导致编译器创建一个对象闭包(C++ 中的函数对象),它可以像普通函数一样传递给for_each (并从中调用)。这两个选项的区别在于[=]安排将每个捕获变量的副本放在对象闭包中;[&]安排将引用放在那里。程序员必须在这两个选项之间进行选择。对于大型对象来说,复制可能很昂贵,并且在创建闭包后对对象所做的任何更改在最终执行时都不会被 lambda 表达式的代码看到。引用允许看到更改,但如果闭包的生命周期超过捕获对象的生命周期,则会导致未定义(并且可能不正确)的行为:C++ 的范围不是无限的。在特别复杂的情况下,程序员可以逐个对象指定捕获:

Both of these cause the compiler to create an object closure (a function object in C++), which could be passed to (and called from) for_each in the same way as an ordinary function. The difference between the two options is that [=] arranges for a copy of each captured variable to be placed in the object closure; [&] arranges for a reference to be placed there instead. The programmer must choose between these options. Copying can be expensive for large objects, and any changes to the object made after the closure is created will not be seen by the code of the lambda expression when it finally executes. References allow changes to be seen, but will lead to undefined (and presumably incorrect) behavior if the closure's lifetime exceeds that of the captured object: C++ does not have unlimited extent. In particularly complex situations, the programmer can specify capture on an object-by-object basis:

[j, &k](int e){ … // 捕获 j 的值和对 k 的引用,

[j, &k](int e){ … // capture j's value and a reference to k,

  // 因此它们可以在这里使用

  // so they can be used in here

设计与实现

Design & Implementation

3.10 函数和函数对象

3.10 Functions and function objects

精明的读者可能会想:在示例 3.40中, for_each如何在其第三个参数的两种不同实现下“做正确的事”?毕竟,有时该参数被实现为一个简单的指针;其他时候它是一个指向带有operator()的对象的指针,这需要不同类型的调用。答案是for_each是一个泛型例程( C++ 中的模板)。编译器根据需要生成for_each的定制实现。我们将在第 7.3.1 节中更详细地讨论泛型。

The astute reader maybe wondering: In Example 3.40, how does for_each manage to “do the right thing” with two different implementations of its third parameter? After all, sometimes that parameter is implemented as a simple pointer; other times it is a pointer to an object with an operator(), which requires a different kind of call. The answer is that for_each is a generic routine (a template in C++). The compiler generates customized implementations of for_each on demand. We will discuss generics in more detail in Section 7.3.1.

在某些情况下,使用泛型来区分“类似函数”的参数可能很困难。作为替代方案,C++ 提供了一个标准函数 ,其构造函数允许从函数、函数指针、函数对象或手动创建的对象闭包实例化它。然后可以将for_each之类的东西写成普通(非泛型)子例程,其第三个参数是函数类的对象在任何给定的调用中,编译器都会将提供的参数强制转换为函数对象。

In some situations, it may be difficult to use generics to distinguish among “function-like” parameters. As an alternative, C++ provides a standard function class, with constructors that allow it to be instantiated from a function, a function pointer, a function object, or a manually created object closure. Something like for_each could then be written as an ordinary (nongeneric) subroutine whose third parameter was a object of class function. In any given call, the compiler would coerce the provided argument to be a function object.

例 3.41

Example 3.41

Java 8 中的 Lambda 表达式

Lambda expressions in Java 8

Lambda 表达式也出现在 Java 8 中,但形式受限。在它们可能有用的情况下,Java 传统上依赖于一种称为函数接口的习惯用法。例如,Arrays.sort例程需要一个Comparator类型的参数。要按年龄对人员记录数组进行排序,我们(传统上)会这样写

Lambda expressions appear in Java 8 as well, but in a restricted form. In situations where they might be useful, Java has traditionally relied on an idiom known as a functional interface. The Arrays.sort routine, for example, expects a parameter of type Comparator. To sort an array of personnel records by age, we would (traditionally) have written

类 AgeComparator 实现 Comparator<Person> {

class AgeComparator implements Comparator<Person> {

 公共 int 比较(人 p1,人 p2){

 public int compare(Person p1, Person p2) {

  返回 Integer.compare(p1.age, p2.age);

  return Integer.compare(p1.age, p2.age);

 }

 }

}

}

人[]人=…

Person[] People = …

数组.sort(People,new AgeComparator());

Arrays.sort(People, new AgeComparator());

值得注意的是,Comparator只有一个抽象方法:AgeComparator类提供的比较例程。使用 Java 8 中的 lambda 表达式,我们可以省略AgeComparator的声明,只需编写

Significantly, Comparator has only a single abstract method: the compare routine provided by our AgeComparator class. With lambda expressions in Java 8, we can omit the declaration of AgeComparator and simply write

数组.sort(People, (p1, p2) -> Integer.compare(p1.age, p2.age));

Arrays.sort(People, (p1, p2) -> Integer.compare(p1.age, p2.age));

语法更简单的关键在于 Comparator 是一个函数式接口,因此只有一个抽象方法。当变量或形式参数被声明为某个函数式接口类型时,Java 8 允许将 lambda 表达式(其参数和返回类型与接口的单个​​方法的参数和返回类型相匹配)赋值给变量或作为参数传递。实际上,编译器使用 lambda 表达式来创建实现接口的匿名类的实例。■

The key to the simpler syntax is that Comparator is a functional interface, and thus has only a single abstract method. When a variable or formal parameter is declared to be of some functional interface type, Java 8 allows a lambda expression whose parameter and return types match those of the interface's single method to be assigned into the variable or passed as the parameter. In effect, the compiler uses the lambda expression to create an instance of an anonymous class that implements the interface. ■

事实证明,强制转换为函数式接口类型是Java 中 lambda 表达式的唯一用途。具体来说,lambda 表达式没有自己的类型:它们不是真正的对象,不能直接操作。它们在变量捕获方面的行为完全由嵌套类的常用规则决定。我们将在第10.2.3 节中更详细地讨论这些规则;目前,只需注意 Java 与 C++ 一样,不支持无限扩展。

As it turns out, coercion to functional interface types is the only use of lambda expressions in Java. In particular, lambda expressions have no types of their own: they are not really objects, and cannot be directly manipulated. Their behavior with respect to variable capture is entirely determined by the usual rules for nested classes. We will consider these rules in more detail in Section 10.2.3; for now, suffice it to note that Java, like C++, does not support unlimited extent.

3.7 宏扩展

3.7 Macro Expansion

例 3.42

Example 3.42

一个简单的汇编宏

A simple assembly macro

在高级编程语言出现之前,汇编语言程序员可能会发现自己编写的代码重复性很高。为了减轻负担,许多汇编程序提供了复杂的宏扩展功能。考虑将二维数组的元素从内存加载到寄存器的任务。正如我们将在第 8.2.3 节中看到的那样,此操作很容易需要六条指令,具体细节取决于硬件指令集;数组元素的大小;以及索引是常量、内存中的值还是寄存器中的值。在许多早期的汇编程序中,可以定义一个宏,用适当的多指令序列替换ld2d(target_reg, array_name, row, column, row_size, element_size)之类的表达式。在包含数百或数千个数组访问操作的数字程序中,这个宏非常有用。■

Prior to the development of high-level programming languages, assembly language programmers could find themselves writing highly repetitive code. To ease the burden, many assemblers provided sophisticated macro expansion facilities. Consider the task of loading an element of a two-dimensional array from memory into a register. As we shall see in Section 8.2.3, this operation can easily require half a dozen instructions, with details depending on the hardware instruction set; the size of the array elements; and whether the indices are constants, values in memory, or values in registers. In many early assemblers, one could define a macro that would replace an expression like ld2d(target_reg, array_name, row, column, row_size, element_size) with the appropriate multi-instruction sequence. In a numeric program containing hundreds or thousands of array access operations, this macro could prove extremely useful. ■

例 3.43

Example 3.43

C 中的预处理器宏

Preprocessor macros in C

当 C 语言在 20 世纪 70 年代初创建时,它自然而然地包含了一个宏预处理功能:

When C was created in the early 1970s, it was natural to include a macro preprocessing facility:

#定义 LINE_LEN 80

#define LINE_LEN 80

#定义除法(a,n)(!((n)%(a)))

#define DIVIDES(a,n) (!((n) % (a)))

 /* 当且仅当 n 模 a 的余数为零,否则为真 */

 /* true iff n has zero remainder modulo a */

#定义 SWAP(a,b) {int t = (a); (a) = (b); (b) = t;}

#define SWAP(a,b) {int t = (a); (a) = (b); (b) = t;}

#定义 MAX(a,b) ((a) > (b) ? (a) : (b))

#define MAX(a,b) ((a) > (b) ? (a) : (b))

像LINE_LEN这样的宏避免了(在早期版本的 C 中)语言本身需要支持命名常量。也许更重要的是,像DIVIDESMAXSWAP这样的参数化宏比等效的 C 函数效率高得多。它们避免了子例程调用机制(包括寄存器保存和恢复)的开销,并且它们生成的代码可以集成到编译器能够在调用周围的代码中实现的任何代码改进中。■

Macros like LINE_LEN avoided the need (in early versions of C) to support named constants in the language itself. Perhaps more important, parameterized macros like DIVIDES, MAX, and SWAP were much more efficient than equivalent C functions. They avoided the overhead of the subroutine call mechanism (including register saves and restores), and the code they generated could be integrated into any code improvements that the compiler was able to effect in the code surrounding the call. ■

例 3.44

Example 3.44

C 宏中的“陷阱”

“Gotchas” in C macros

遗憾的是,C 宏存在一些局限性,所有这些局限性都源于它们通过文本替换实现,并且不被编译器的其余部分理解。换句话说,它们提供了一种与编程语言的其余部分分离且通常不一致的命名和绑定机制。

Unfortunately, C macros suffer from several limitations, all of which stem from the fact that they are implemented by textual substitution, and are not understood by the rest of the compiler. Put another way, they provide a naming and binding mechanism that is separate from—and often at odds with—the rest of the programming language.

在DIVIDES的定义中, an出现的位置周围的括号是必不可少的。如果没有它们,DIVIDES(y + z, x)将被替换为 (!(x % y + z)),根据优先规则,这与(!((x % y) + z))相同。类似地,如果程序员编写SWAP(x, t) ,SWAP可能会出现意外行为:参数的文本替换允许宏的t声明捕获传递的t。MAX (x++, y++)也可能出现意外行为,因为增量副作用将发生多次。不幸的是,在标准 C 中,我们无法通过将参数分配给临时变量来避免额外的副作用:返回值的 C 宏必须是一个表达式,并且声明是不能出现在内部的许多语言结构之一(另请参见练习 3.23)。■

In the definition of DIVIDES, the parentheses around the occurrences of a and n are essential. Without them, DIVIDES(y + z, x) would be replaced by (!(x % y + z)), which is the same as (!((x % y) + z)), according to the rules of precedence. In a similar vein, SWAP may behave unexpectedly if the programmer writes SWAP(x, t): textual substitution of arguments allows the macro's declaration of t to capture the t that was passed. MAX(x++, y++) may also behave unexpectedly, since the increment side effects will happen more than once. Unfortunately, in standard C we cannot avoid the extra side effects by assigning the parameters into temporary variables: a C macro that “returns” a value must be an expression, and declarations are one of many language constructs that cannot appear inside (see also Exercise 3.23). ■

设计与实现

Design & Implementation

3.11 泛型作为宏

3.11 Generics as macros

从某种意义上说,将名称导入普通模块的能力提供了一种原始的通用功能。例如,导入其元素类型的堆栈模块可以插入(使用文本编辑器)到已声明适当类型名称的任何上下文中,并在编译时为该上下文生成“自定义”堆栈。早期版本的 C++ 通过使用宏来实现模板来形式化此机制。更高版本的 C++ 已将模板(泛型)作为完全支持的语言功能,使其具有卫生宏的许多特点。(有关模板和模板元编程的更多信息,请参阅第 C-7.3.2 节。)

In some sense, the ability to import names into an ordinary module provides a primitive sort of generic facility. A stack module that imports its element type, for example, can be inserted (with a text editor) into any context in which the appropriate type name has been declared, and will produce a “customized” stack for that context when compiled. Early versions of C++ formalized this mechanism by using macros to implement templates. Later versions of C++ have made templates (generics) a fully supported language feature, giving them much of the flavor of hygienic macros. (More on templates and on template metaprogramming can be found in Section C-7.3.2.)

现代语言和编译器大多已将宏视为过时之物而弃之不用。命名常量是类型安全的且易于实现,而内联子例程(将在9.2.4 节中讨论)几乎提供了参数化宏的所有性能,而没有参数化宏的限制。少数语言(尤其是 Scheme 和 Common Lisp)采用了另一种方法,以安全一致的方式将宏集成到语言中。所谓的卫生宏隐式封装其参数,避免与结合性和优先级发生意外交互。它们会在必要时重命名变量以避免捕获问题,并且可以在任何表达式上下文中使用它们。然而,与子例程不同,它们在语义分析期间会扩展,因此通常不适合无界递归。它们的吸引力在于,与所有宏一样,它们采用未求值的参数,并根据需要对其进行惰性求值。除其他外,这意味着它们保留了我们的 MAX 示例中的多个副作用“陷阱”。延迟求值在此上下文中是一个错误,但有时可以成为一项功能。我们将在第 6.1.5 节(短路布尔求值)、第 9.3.2 节(按名称调用参数)和第 11.5 节(函数式编程语言中的正常顺序求值)中回顾它。

Modern languages and compilers have, for the most part, abandoned macros as an anachronism. Named constants are type-safe and easy to implement, and in-line subroutines (to be discussed in Section 9.2.4) provide almost all the performance of parameterized macros without their limitations. A few languages (notably Scheme and Common Lisp) take an alternative approach, and integrate macros into the language in a safe and consistent way. So-called hygienic macros implicitly encapsulate their arguments, avoiding unexpected interactions with associativity and precedence. They rename variables when necessary to avoid the capture problem, and they can be used in any expression context. Unlike subroutines, however, they are expanded during semantic analysis, making them generally unsuitable for unbounded recursion. Their appeal is that, like all macros, they take unevaluated arguments, which they evaluate lazily on demand. Among other things, this means that they preserve the multiple side effect “gotcha” of our MAX example. Delayed evaluation was a bug in this context, but can sometimes be a feature. We will return to it in Sections 6.1.5 (short-circuit Boolean evaluation), 9.3.2 (call-by-name parameters), and 11.5 (normal-order evaluation in functional programming languages).

03-01-9780124104099检查你的理解

Check Your Understanding

34.描述 引用环境的深绑定和浅绑定之间的区别。

34. Describe the difference between deep and shallow binding of referencing environments.

35. 为什么绑定规则对于具有动态作用域的语言特别重要?

35. Why are binding rules particularly important for languages with dynamic scoping?

36. 什么是一等子程序?哪些语言支持它们?

36. What are first-class subroutines? What languages support them?

37. 什么是子程序闭包?它有什么用?它是如何实现的?

37. What is a subroutine closure? What is it used for? How is it implemented?

38. 什么是对象闭包? 它和子程序闭包有什么关系?

38. What is an object closure? How is it related to a subroutine closure?

39. 描述C# 的委托如何扩展和统一子程序和对象闭包。

39. Describe how the delegates of C# extend and unify both subroutine and object closures.

40.解释 局部范围内对象有限范围和无限范围的区别。

40. Explain the distinction between limited and unlimited extent of objects in a local scope.

41. 什么是lambda 表达式?与 C# 或 Ruby 相比,函数式语言对 lambda 表达式的支持如何?与 C++ 或 Java 相比如何?

41. What is a lambda expression? How does the support for lambda expressions in functional languages compare to that of C# or Ruby? To that of C++ or Java?

42. 什么是?将它们纳入 C 语言的动机是什么?它们可能引发什么问题?

42. What are macros? What was the motivation for including them in C? What problems may they cause?

3.8 单独编译

3.8 Separate Compilation

由于大多数大型程序都是逐步构建和测试的,并且非常大的程序的编译可能需要几个小时,因此任何旨在支持大型程序的语言都必​​须提供单独的编译。

Since most large programs are constructed and tested incrementally, and since the compilation of a very large program can be a multihour operation, any language designed to support large programs must provide for separate compilation.

03-02-9780124104099 更深入地

IN MORE DEPTH

在配套网站上,我们考虑了模块和单独编译之间的关系。由于模块是为封装而设计的,并且提供了狭窄的接口,因此它们自然而然地成为许多编程语言的“编译单元”的选择。例如,Modula-3 和 Ada 的单独模块头和模块体明确用于单独编译,并反映了在其他语言中使用更原始功能的经验。相比之下,C 和 C++ 必须保持与 20 世纪 70 年代早期设计的机制的向后兼容性。C 和 C++ 的现代版本包含一个提供类似模块的数据隐藏的命名空间机制,但名称在每个编译单元中使用之前仍必须声明,而用于适应此规则的机制纯粹是惯例问题。Java 和 C# 打破了 C 传统,要求编译器从单独编译的类定义中自动推断头信息;不需要头文件。

On the companion site we consider the relationship between modules and separate compilation. Because they are designed for encapsulation and provide a narrow interface, modules are the natural choice for the “compilation units” of many programming languages. The separate module headers and bodies of Modula-3 and Ada, for example, are explicitly intended for separate compilation, and reflect experience gained with more primitive facilities in other languages. C and C++, by contrast, must maintain backward compatibility with mechanisms designed in the early 1970s. Modern versions of C and C++ include a namespace mechanism that provides module-like data hiding, but names must still be declared before they are used in every compilation unit, and the mechanisms used to accommodate this rule are purely a matter of convention. Java and C# break with the C tradition by requiring the compiler to infer header information automatically from separately compiled class definitions; no header files are required.

3.9 总结和结束语

3.9 Summary and Concluding Remarks

本章讨论了名称以及名称与对象(广义上)的绑定。我们首先对绑定时间的概念进行了一般性讨论——绑定时间是指名称与特定对象相关联的时间,或者更一般地说,绑定时间是指答案与语言或程序设计或实现中的任何未决问题相关联的时间。我们为对象和名称与对象绑定定义了生存期的概念,并指出它们不必相同。然后,​​我们介绍了用于管理对象空间的三种主要存储分配机制——静态、堆栈和堆。

This chapter has addressed the subject of names, and the binding of names to objects (in a broad sense of the word). We began with a general discussion of the notion of binding time—the time at which a name is associated with a particular object or, more generally, the time at which an answer is associated with any open question in language or program design or implementation. We defined the notion of lifetime for both objects and name-to-object bindings, and noted that they need not be the same. We then introduced the three principal storage allocation mechanisms—static, stack, and heap—used to manage space for objects.

第 3.3 节中,我们描述了名称与对象的绑定如何受作用域规则的支配。在某些语言中,作用域规则是动态的:名称的含义位于最近进入的包含声明且尚未退出的作用域中。然而,在大多数现代语言中,作用域规则是静态的或词汇的:名称的含义位于词汇上最接近的包含声明的作用域中。我们发现词汇作用域规则在不同语言之间存在重要但有时微妙的差异。我们考虑了哪些类型的作用域可以嵌套,作用域是开放的还是封闭的,名称的作用域是否包含声明它的整个块,以及名称必须在使用前声明。我们在3.4 节中探讨了范围规则的实现。

In Section 3.3 we described how the binding of names to objects is governed by scope rules. In some languages, scope rules are dynamic: the meaning of a name is found in the most recently entered scope that contains a declaration and that has not yet been exited. In most modern languages, however, scope rules are static, or lexical: the meaning of a name is found in the closest lexically surrounding scope that contains a declaration. We found that lexical scope rules vary in important but sometimes subtle ways from one language to another. We considered what sorts of scopes are allowed to nest, whether scopes are open or closed, whether the scope of a name encompasses the entire block in which it is declared, and whether a name must be declared before it is used. We explored the implementation of scope rules in Section 3.4.

第 3.5 节中,我们研究了绑定相互关联的几种方式。当给定范围内的两个或多个名称绑定到同一个对象时,就会出现别名。当一个名称绑定到多个对象时,就会出现重载。我们注意到,虽然有时可以通过强制或多态实现类似于重载的行为,但底层机制实际上非常不同。在第 3.6 节中,我们考虑了何时将引用环境绑定到作为参数传递、从函数返回或存储在变量中的子例程的问题。我们的讨论涉及闭包lambda 表达式的概念,这两者都将在后面的章节中反复出现。在第 3.73.8节中,我们考虑了宏和单独编译。

In Section 3.5 we examined several ways in which bindings relate to one another. Aliases arise when two or more names in a given scope are bound to the same object. Overloading arises when one name is bound to multiple objects. We noted that while behavior reminiscent of overloading can sometimes be achieved through coercion or polymorphism, the underlying mechanisms are really very different. In Section 3.6 we considered the question of when to bind a referencing environment to a subroutine that is passed as a parameter, returned from a function, or stored in a variable. Our discussion touched on the notions of closures and lambda expressions, both of which will appear repeatedly in later chapters. In Sections 3.7 and 3.8 we considered macros and separate compilation.

词法作用域的一些更复杂的方面说明了语言对数据抽象支持的演变,我们将在第 10 章中回到这个主题。我们首先描述了 Fortran、Algol 60 和 C 等语言的自身或静态变量,这些变量允许子程序的本地变量在一次调用到下一次调用之间保留其值。然后我们注意到,简单模块可以看作是一种将长寿命对象本地化为一组子程序的方法,这样它们对程序的其他部分就不可见了。通过有选择地导出名称,模块可以充当一种或多种抽象数据类型的“管理器”。在下一个复杂程度下,我们注意到有些语言将模块视为类型,允许程序员创建任意数量的模块定义的抽象实例。最后,我们注意到面向对象语言通过提供继承机制扩展了模块即类型方法(以及词法范围的概念),该机制允许将新的抽象(类)定义为现有类的扩展或改进。

Some of the more complicated aspects of lexical scoping illustrate the evolution of language support for data abstraction, a subject to which we will return in Chapter 10. We began by describing the own or static variables of languages like Fortran, Algol 60, and C, which allow a variable that is local to a subroutine to retain its value from one invocation to the next. We then noted that simple modules can be seen as a way to make long-lived objects local to a group of subroutines, in such a way that they are not visible to other parts of the program. By selectively exporting names, a module may serve as the “manager” for one or more abstract data types. At the next level of complexity, we noted that some languages treat modules as types, allowing the programmer to create an arbitrary number of instances of the abstraction defined by a module. Finally, we noted that object-oriented languages extend the module-as-type approach (as well as the notion of lexical scope) by providing an inheritance mechanism that allows new abstractions (classes) to be defined as extensions or refinements of existing classes.

在本章讨论的主题中,我们看到了几个有用的特性(递归、静态作用域、前向引用、一等子程序、无限范围)的例子,由于担心实现复杂性或运行时成本,某些语言已经省略了这些特性。我们还看到了一个特性的例子(模块规范的私有部分),它是为了方便语言的实现而专门引入的,另一个特性(C 中的单独编译)的设计显然是为了反映特定的实现。在语言设计的其他几个方面(后期绑定与早期绑定、静态作用域与动态作用域、对强制和转换的支持、对指针和其他别名的容忍度),我们看到实现问题起着重要作用。

Among the topics considered in this chapter, we saw several examples of useful features (recursion, static scoping, forward references, first-class subroutines, unlimited extent) that have been omitted from certain languages because of concern for their implementation complexity or run-time cost. We also saw an example of a feature (the private part of a module specification) introduced expressly to facilitate a language's implementation, and another (separate compilation in C) whose design was clearly intended to mirror a particular implementation. In several additional aspects of language design (late vs early binding, static vs dynamic scoping, support for coercions and conversions, toleration of pointers and other aliases), we saw that implementation issues play a major role.

同样,看似简单的语言规则也可能产生意想不到的影响。例如,在第 3.3.3 节中,我们考虑了整个块作用域与名称必须先声明后才能使用的要求之间的相互作用。与 Fortran 的 do 循环语法和空格规则(第 2.2.2 节)或Pascal 的if…then…else语法(第 2.3.2 节)一样,选择不当的作用域规则不仅会使编译器难以进行程序分析,而且会使人类也难以进行程序分析。在后续章节中,我们将看到几个令人困惑且难以编译的功能的其他示例。当然,语义效用和易用性实现并不总是齐头并进。许多易于编译的功能(例如 goto 语句)充其量也只是值得怀疑的价值。我们还将看到几个非常有用且(概念上)简单的功能示例,例如垃圾收集(第 8.5.3 节)和统一(第 7.2.4 节C-7.3.2 节12.2.1 节),它们的实现相当复杂。

In a similar vein, apparently simple language rules can have surprising implications. In Section 3.3.3, for example, we considered the interaction of whole-block scope with the requirement that names be declared before they can be used. Like the do loop syntax and white space rules of Fortran (Section 2.2.2) or the if… then …else syntax of Pascal (Section 2.3.2), poorly chosen scoping rules can make program analysis difficult not only for the compiler, but for human beings as well. In future chapters we shall see several additional examples of features that are both confusing and hard to compile. Of course, semantic utility and ease of implementation do not always go together. Many easy-to-compile features (e.g., goto statements) are of questionable value at best. We will also see several examples of highly useful and (conceptually) simple features, such as garbage collection (Section 8.5.3) and unification (Sections 7.2.4, C-7.3.2, and 12.2.1), whose implementations are quite complex.

3.10 练习

3.10 Exercises

3.1 指出您最喜欢的编程语言和实现中下列每个决策的约束时间(语言设计时、程序链接时、程序开始执行时等)。解释您认为任何有待解释的答案。

3.1 Indicate the binding time (when the language is designed, when the program is linked, when the program begins execution, etc.) for each of the following decisions in your favorite programming language and implementation. Explain any answers you think are open to interpretation.

 内置函数的数量(数学、类型查询等)

 The number of built-in functions (math, type queries, etc.)

 与特定变量引用对应的变量声明(使用)

 The variable declaration that corresponds to a particular variable reference (use)

 常量(文字)字符串的最大长度

 The maximum length allowed for a constant (literal) character string

 作为参数传递的子程序的引用环境

 The referencing environment for a subroutine that is passed as a parameter

 特定库例程的地址

 The address of a particular library routine

 程序代码和数据占用的空间总量

 The total amount of space occupied by program code and data

3.2 在 Fortran 77 中,局部变量通常是静态分配的。在 Algol 及其后代(例如 Ada 和 C)中,它们通常是在堆栈中分配的。在 Lisp 中,它们通常是至少部分地在堆中分配的。这些差异是如何造成的?举一个 Ada 或 C 程序的例子,如果局部变量是静态分配的,该程序将无法正常工作。举一个 Scheme 或 Common Lisp 程序的例子,如果局部变量是在堆栈中分配的,该程序将无法正常工作。

3.2 In Fortran 77, local variables were typically allocated statically. In Algol and its descendants (e.g., Ada and C), they are typically allocated in the stack. In Lisp they are typically allocated at least partially in the heap. What accounts for these differences? Give an example of a program in Ada or C that would not work correctly if local variables were allocated statically. Give an example of a program in Scheme or Common Lisp that would not work correctly if local variables were allocated on the stack.

3.3 举两个例子,说明推迟执行决定的约束力可能是有意义的,尽管有足够的信息可以尽早执行该决定。

3.3 Give two examples in which it might make sense to delay the binding of an implementation decision, even though sufficient information exists to bind it early.

3.4 从你熟悉的编程语言中给出三个具体的例子,其中变量是有效的但不在范围内。

3.4 Give three concrete examples drawn from programming languages with which you are familiar in which a variable is live but not in scope.

3.5 考虑以下伪代码:

1. 过程 main()

2. a : integer := 1

3. b : integer := 2

4. 过程 middle()

5. b : integer := a

6. 过程 inner()

7. 打印 a, b

8. a : integer := 3

9. –– 中间部分

10. inner()

11. 打印 a, b

12. –– 主部分

13. middle()

14. 打印 a, b

假设这是具有 C 的声明顺序规则(但具有嵌套子例程)的语言的代码 - 即,名称必须在使用前声明,并且名称的范围从其声明延伸到块的末尾。在每个打印语句中,指出ab的哪些声明在引用环境中。程序会打印什么(或者编译器会识别静态语义错误)?重复练习 C# 的声明顺序规则(名称必须在使用前声明,但名称的范围是声明它的整个块)和 Modula-3(名称可以按任何顺序声明,并且它们的范围是声明它们的整个块)。

3.5 Consider the following pseudocode:

1.  procedure main()

2.    a : integer := 1

3.    b : integer := 2

4.    procedure middle()

5.      b : integer := a

6.      procedure inner()

7.        print a, b

8.        a : integer := 3

9.        – – body of middle

10.        inner()

11.        print a, b

12.      –– body of main

13.       middle()

14.       print a, b

Suppose this was code for a language with the declaration-order rules of C (but with nested subroutines)—that is, names must be declared before use, and the scope of a name extends from its declaration through the end of the block. At each print statement, indicate which declarations of a and b are in the referencing environment. What does the program print (or will the compiler identify static semantic errors)? Repeat the exercise for the declaration-order rules of C# (names must be declared before use, but the scope of a name is the entire block in which it is declared) and of Modula-3 (names can be declared in any order, and their scope is the entire block in which they are declared).

3.6 考虑以下伪代码,假设嵌套子程序和静态范围:过程 main() g : 整数过程 B(a : 整数) x : 整数过程 A(n : 整数) g := n过程 R(m : 整数) write_integer(x) x /:= 2 ––如果 x > 1 ,则为整数除法R(m + 1)否则A(m) –– B 的主体x := a × a R(1) –– main 的主体B(3) write_integer(g)

 

  

  

   

   

    

   

    

    

    

     

    

     

   

   

   

  

  

  

3.6 Consider the following pseudocode, assuming nested subroutines and static scope:

 procedure main()

  g : integer

  procedure B(a : integer)

   x : integer

   procedure A(n : integer)

    g := n

   procedure R(m : integer)

    write_integer(x)

    x /:= 2 –– integer division

    if x > 1

     R(m + 1)

    else

     A(m)

   –– body of B

   x := a × a

   R(1)

  –– body of main

  B(3)

  write_integer(g)

(a) 该程序打印什么?

(a) What does this program print?

(b)显示 A刚被调用时堆栈上的框架。对于每个框架,显示静态和动态链接。

(b) Show the frames on the stack when A has just been called. For each frame, show the static and dynamic links.

(c) 解释A如何找到g

(c) Explain how A finds g.

3.7 作为 MumbleTech.com 开发团队的一员,Janet 编写了一个 C 语言列表操作库,其中包含图 3.16中的代码。

3.7 As part of the development team at MumbleTech.com, Janet has written a list manipulation library for C that contains, among other things, the code in Figure 3.16.

传真:03-16-9780124104099
图 3.16 列出 练习 3.7 的管理例程。

(a) 新团队成员 Brad 习惯于 Java,他在程序的主循环中包含以下代码:list_node* L = 0; while (more_widgets()) { L = insert(next_widget(), L); } L = reverse(L);

 

 

  

 

 

遗憾的是,运行一段时间后,Brad 的程序总是内存不足并崩溃。请解释一下问题出在哪里。

(a) Accustomed to Java, new team member Brad includes the following code in the main loop of his program:

 list_node* L = 0;

 while (more_widgets()) {

  L = insert(next_widget(), L);

 }

 L = reverse(L);

Sadly, after running for a while, Brad's program always runs out of memory and crashes. Explain what's going wrong.

(b) 在 Janet 耐心地向 Brad 解释完问题后,Brad 又尝试了一次:list_node* L = 0; while (more_widgets()) { L = insert(next_widget(), L); } list_node* T = reverse(L); delete_list(L); L = T;这似乎解决了内存不足的问题,但是程序过去会产生正确的结果(在内存耗尽之前),现在其输出却奇怪地损坏了,Brad 又回到 Janet 那里寻求建议。这次她会告诉他什么呢?

 

 

  

 

 

 

 

(b) After Janet patiently explains the problem to him, Brad gives it another try:

 list_node* L = 0;

 while (more_widgets()) {

  L = insert(next_widget(), L);

 }

 list_node* T = reverse(L);

 delete_list(L);

 L = T;

This seems to solve the insufficient memory problem, but where the program used to produce correct results (before running out of memory), now its output is strangely corrupted, and Brad goes back to Janet for advice. What will she tell him this time?

3.8用 C 语言 重写图 3.63.7。您需要使用单独的编译来隐藏名称。

3.8 Rewrite Figures 3.6 and 3.7 in C. You will need to use separate compilation for name hiding.

3.9 考虑以下 C 语言代码片段:

{ int a, b, c; { int d, e; { int f; } } { int g, h, i; } }

 



  

  

   

  

  

 

 

 

  

 

 

3.9 Consider the following fragment of code in C:

{ int a, b, c;

 

{ int d, e;

  

  { int f;

   

  }

  

 }

 

 { int g, h, i;

  

 }

 

}

(a) 假设每个整型变量占用四个字节。这段代码中的变量总共需要多少空间?

(a) Assume that each integer variable occupies four bytes. How much total space is required for the variables in this code?

(b) 描述一种算法,编译器可以使用该算法将堆栈帧偏移量分配给任意嵌套块的变量,以最小化所需的总空间。

(b) Describe an algorithm that a compiler could use to assign stack frame offsets to the variables of arbitrary nested blocks, in a way that minimizes the total space required.

3.10 考虑 Fortran 77 编译器的设计,该编译器对子例程的局部变量使用静态分配。扩展上一个问题的答案,描述一种算法来最小化这些变量所需的总空间。您可能会发现构建调用图数据很有帮助结构中每个节点代表一个子程序,每条有向弧表示尾部的子程序有时可能会调用头部的子程序。

3.10 Consider the design of a Fortran 77 compiler that uses static allocation for the local variables of subroutines. Expanding on the solution to the previous question, describe an algorithm to minimize the total space required for these variables. You may find it helpful to construct a call graph data structure in which each node represents a subroutine, and each directed arc indicates that the subroutine at the tail may sometimes call the subroutine at the head.

3.11 考虑以下伪代码:

过程 P(A, B : real) X : real过程 Q(B, C : real) Y : real 过程 R(A, C : real) Z : real … –– (*) 假设静态范围,(*) 标记位置的引用环境是什么?

 

 

  

  

 

  

  

 

3.11 Consider the following pseudocode:

procedure P(A, B : real)

 X : real

 procedure Q(B, C : real)

  Y : real

  

 procedure R(A, C : real)

  Z : real

  … –– (*)

 

Assuming static scope, what is the referencing environment at the location marked by (*)?

3.12 用 Scheme 编写一个简单的程序,根据我们使用letlet*还是letrec来声明给定的名称集,显示三种不同的行为。(提示:为了充分利用letrec,你可能希望你的名字是函数 [ lambda表达式]。)

3.12 Write a simple program in Scheme that displays three different behaviors, depending on whether we use let, let*, or letrec to declare a given set of names. (Hint: To make good use of letrec, you will probably want your names to be functions [lambda expressions].)

3.13 考虑以下 Scheme 程序:

(define A (lambda() (let* ((x 2) (C (lambda (P) (let ((x 4)) (P)))) (D (lambda () x)) (B (lambda () (let ((x 3)) (CD))))) (B))))该程序打印什么?如果 Scheme 使用动态作用域和浅绑定,它会打印什么?动态作用域和深绑定?解释你的答案。

 

  

   

    

     

   

    

   

    

     

  

3.13 Consider the following program in Scheme:

(define A

 (lambda()

  (let* ((x 2)

   (C (lambda (P)

    (let ((x 4))

     (P))))

   (D (lambda ()

    x))

   (B (lambda ()

    (let ((x 3))

     (C D)))))

  (B))))

What does this program print? What would it print if Scheme used dynamic scoping and shallow binding? Dynamic scoping and deep binding? Explain your answers.

3.14 考虑以下伪代码:

x : 整数 –– 全局

过程集 x(n : 整数) x := n过程 print_x()写入整数(x)过程 first() set_x(1) print_x()过程 second() x : 整数set_x(2) print_x() set_x(0) first() print_x() second() print_x()

 



 



 

 



 

 

 











如果语言使用静态作用域,该程序会打印什么?如果使用动态作用域,它会打印什么?为什么?

3.14 Consider the following pseudocode:

x : integer –– global

procedure set x(n : integer)

 x := n

procedure print_x()

 write integer(x)

procedure first()

 set_x(1)

 print_x()

procedure second()

 x : integer

 set_x(2)

 print_x()

set_x(0)

first()

print_x()

second()

print_x()

What does this program print if the language uses static scoping? What does it print with dynamic scoping? Why?

3.15 支持动态作用域的主要论点是它有助于定制子例程。例如,假设我们有一个库例程print_integer,它能够以几种进制(十进制、二进制、十六进制等)中的任意一种打印其参数。进一步假设我们希望例程大多数时候都使用十进制表示法,并且只在少数特殊情况下使用其他进制:我们不想在每次单独调用时都明确指定进制。我们可以通过动态作用域来实现此结果,方法是让print_integer从非局部变量print_base获取其进制。我们可以通过在执行初期遇到的作用域中声明变量print_base并将其值设置为 10 来建立默认行为。然后,任何时候我们想要临时更改基数,我们都可以这样写:

begin – – 嵌套块print_base : integer := 16 – – 使用十六进制print_integer(n)此参数的问题在于,通常还有其他方法可以实现相同的效果,而无需动态作用域。请至少描述两个print_integer示例。

 

 

3.15 The principal argument in favor of dynamic scoping is that it facilitates the customization of subroutines. Suppose, for example, that we have a library routine print_integer that is capable of printing its argument in any of several bases (decimal, binary, hexadecimal, etc.). Suppose further that we want the routine to use decimal notation most of the time, and to use other bases only in a few special cases: we do not want to have to specify a base explicitly on each individual call. We can achieve this result with dynamic scoping by having print_integer obtain its base from a nonlocal variable print_base. We can establish the default behavior by declaring a variable print_base and setting its value to 10 in a scope encountered early in execution. Then, any time we want to change the base temporarily, we can write

begin – – nested block

 print_base : integer := 16 – – use hexadecimal

 print_integer(n)

The problem with this argument is that there are usually other ways to achieve the same effect, without dynamic scoping. Describe at least two for the print_integer example.

3.16如 第 3.6.3 节所述,C# 对一流子例程的支持异常复杂。除其他外,它允许从匿名嵌套方法实例化委托,并在此类委托可能需要局部变量和参数时为其提供无限范围。考虑以下 C# 程序中这些功能的含义:

using System;

public delegate int UnaryOp(int n); // 类型声明:UnaryOp 是一个从 int 到 int 的函数public class Foo { static int a = 2; static UnaryOp b(int c) { int d = a + c; Console.WriteLine(d); return delegate(int n) { return c + n; }; } public static void Main(string[] args) { Console.WriteLine(b(3)(4)); } }

 



 

 

  

  

  

 

 

  

 



这个程序打印了什么?abcd中的哪一个(如果有的话)可能被静态分配?哪一个可以在堆栈上分配?哪一个需要在堆上分配?解释一下。

3.16 As noted in Section 3.6.3, C# has unusually sophisticated support for first-class subroutines. Among other things, it allows delegates to be instantiated from anonymous nested methods, and gives local variables and parameters unlimited extent when they may be needed by such a delegate. Consider the implications of these features in the following C# program:

using System;

public delegate int UnaryOp(int n);

 // type declaration: UnaryOp is a function from ints to ints

public class Foo {

 static int a = 2;

 static UnaryOp b(int c) {

  int d = a + c;

  Console.WriteLine(d);

  return delegate(int n) { return c + n; };

 }

 public static void Main(string[] args) {

  Console.WriteLine(b(3)(4));

 }

}

What does this program print? Which of a, b, c, and d, if any, is likely to be statically allocated? Which could be allocated on the stack? Which would need to be allocated in the heap? Explain.

3.17 如果您熟悉结构化异常处理(如 Ada、C++、Java、C#、ML、Python 或 Ruby 中提供),请考虑此机制与范围问题的关系。通常,raisethrow语句被认为是引用异常,它将异常作为参数传递给处理程序查找库例程。在上述每种语言中,异常本身都必须在某个周围范围内声明,并遵守通常的静态范围规则。描述另一种观点,其中 raisethrow实际上是对处理程序的引用,它将控制权直接转移到该处理程序。从这个角度来看,处理程序的范围规则是什么?这些规则与语言的其余部分一致吗?解释一下。(有关异常的更多信息,请参见第 9.4 节。)

3.17 If you are familiar with structured exception handling, as provided in Ada, C++, Java, C#, ML, Python, or Ruby, consider how this mechanism relates to the issue of scoping. Conventionally, a raise or throw statement is thought of as referring to an exception, which it passes as a parameter to a handler-finding library routine. In each of the languages mentioned, the exception itself must be declared in some surrounding scope, and is subject to the usual static scope rules. Describe an alternative point of view, in which the raise or throw is actually a reference to a handler, to which it transfers control directly. Assuming this point of view, what are the scope rules for handlers? Are these rules consistent with the rest of the language? Explain. (For further information on exceptions, see Section 9.4.)

3.18 考虑以下伪代码:

x : 整数 – – 全局

过程 set_x(n : 整数) x := n过程 print_x() write_integer(x)过程 foo(S, P : 函数; n : 整数) x : 整数 := 5 if n in {1, 3} set_x(n) else S(n) if n in {1, 2} print_x() else P set_x(0); foo(set_x, print_x, 1); print_x() set_x(0); foo(set_x, print_x, 2); print_x() set_x(0); foo(set_x, print_x, 3); print_x() set_x(0); foo(set_x, print_x, 4); print_x()

 



 



 

 

  

 

  

 

  

 

  









假设该语言使用动态作用域。如果该语言使用浅绑定,程序会打印什么?如果使用深绑定,程序会打印什么?为什么?

3.18 Consider the following pseudocode:

x : integer – – global

procedure set_x(n : integer)

 x := n

procedure print_x()

 write_integer(x)

procedure foo(S, P : function; n : integer)

 x : integer := 5

 if n in {1, 3}

  set_x(n)

 else

  S(n)

 if n in {1, 2}

  print_x()

 else

  P

set_x(0); foo(set_x, print_x, 1); print_x()

set_x(0); foo(set_x, print_x, 2); print_x()

set_x(0); foo(set_x, print_x, 3); print_x()

set_x(0); foo(set_x, print_x, 4); print_x()

Assume that the language uses dynamic scoping. What does the program print if the language uses shallow binding? What does it print with deep binding? Why?

3.19 考虑以下伪代码:

x : integer := 1

y : integer := 2

procedure add() x := x + y procedure second(P : procedure) x : integer := 2 P() procedure first y : integer := 3 second(add) first() write_integer(x)

 



 

 



 

 



3.19 Consider the following pseudocode:

x : integer := 1

y : integer := 2

procedure add()

 x := x + y

procedure second(P : procedure)

 x : integer := 2

 P()

procedure first

 y : integer := 3

 second(add)

first()

write_integer(x)

(a) 如果该语言使用静态作用域,该程序会打印什么?

(a) What does this program print if the language uses static scoping?

(b) 如果该语言使用具有深度绑定的动态作用域,它会打印什么?

(b) What does it print if the language uses dynamic scoping with deep binding?

(c) 如果该语言使用动态作用域和浅绑定,它会打印什么?

(c) What does it print if the language uses dynamic scoping with shallow binding?

3.20 考虑像 C++ 这样的语言中的数学运算,它既支持重载也支持强制转换。在许多情况下,为一个函数提供多个重载版本是有意义的,每个版本对应一个数值类型或多个类型的组合。在其他情况下,我们可能会使用单个版本(可能为双精度浮点参数定义),并依靠强制转换允许该函数用于其他数值类型(例如整数)。举一个例子,其中重载显然是首选方法。再举一个例子,其中强制转换几乎肯定更好。

3.20 Consider mathematical operations in a language like C++, which supports both overloading and coercion. In many cases, it may make sense to provide multiple, overloaded versions of a function, one for each numeric type or combination of types. In other cases, we might use a single version—probably defined for double-precision floating point arguments—and rely on coercion to allow that function to be used for other numeric types (e.g., integers). Give an example in which overloading is clearly the preferable approach. Give another in which coercion is almost certainly better.

3.21 在支持运算符重载的语言中,构建对有理数的支持。每个数字应在内部以最简单形式表示为(分子,分母)对,分母为正。您的代码应支持一元否定和四个标准算术运算符。为了获得额外分数,请创建一个转换例程,该例程接受两个浮点参数(一个值和一个错误界限),并返回给定值的给定错误界限内最简单(分母最小)的有理数。

3.21 In a language that supports operator overloading, build support for rational numbers. Each number should be represented internally as a (numerator, denominator) pair in simplest form, with a positive denominator. Your code should support unary negation and the four standard arithmetic operators. For extra credit, create a conversion routine that accepts two floating-point parameters—a value and a error bound—and returns the simplest (smallest denominator) rational number within the given error bound of the given value.

3.22 在具有 lambda 表达式的命令式语言(例如 C#、Ruby、C++ 或 Java)中,编写以下高级函数。(我们将在第 11 章中看到,高级函数将其他函数作为参数和/或返回一个函数作为结果。)

3.22 In an imperative language with lambda expressions (e.g., C#, Ruby, C++, or Java), write the following higher-level functions. (A higher-level function, as we shall see in Chapter 11, takes other functions as argument and/or returns a function as a result.)

 compose(g, f) — 返回一个函数 h,使得h(x) ==g(f(x))

 compose(g, f)—returns a function h such that h(x) ==g(f(x)).

 map(f, L) — 给定一个函数f和一个列表L,返回一个列表M,使得M的第 i 个元素是f( e ),其中e是L的第 i 个元素。

 map(f, L)—given a function f and a list L returns a list M such that the ith element of M is f(e), where e is the ith element of L.

 filter(L, P) — 给定一个列表L和一个谓词(布尔返回函数)P ,返回一个列表,该列表包含L中所有且仅包含P为真的元素。

 filter(L, P)—given a list L and a predicate (Boolean-returning function) P, returns a list containing all and only those elements of L for which P is true.

理想情况下,您的代码应该适用于任何参数或列表元素类型。

Ideally, your code should work for any argument or list element type.

3.23 能否用标准 C 编写一个宏,在不调用子程序的情况下“返回”一对参数的最大公约数?为什么可以或为什么不可以?

3.23 Can you write a macro in standard C that “returns” the greatest common divisor of a pair of arguments, without calling a subroutine? Why or why not?

03-02-9780124104099 3.24–3.31  更深入。

3.24–3.31  In More Depth.

3.11 探索

3.11 Explorations

3.32 用你最喜欢的编程语言试验命名规则。阅读手册,编写并编译一些测试程序。该语言使用词法作用域还是动态作用域?作用域可以嵌套吗?它们是开放的还是封闭的?名称的作用域是否包含声明它的整个块,还是仅包含声明后的部分?如何声明相互递归的类型或子程序?子程序可以作为参数传递、从函数返回或存储在变量中吗?如果可以,引用环境何时绑定?

3.32 Experiment with naming rules in your favorite programming language. Read the manual, and write and compile some test programs. Does the language use lexical or dynamic scoping? Can scopes nest? Are they open or closed? Does the scope of a name encompass the entire block in which it is declared, or only the portion after the declaration? How does one declare mutually recursive types or subroutines? Can subroutines be passed as parameters, returned from functions, or stored in variables? If so, when are referencing environments bound?

3.33 列出一种或多种编程语言的关键字(保留字)。列出预定义标识符。(回想一下,每个关键字都是一个单独的标记。标识符不能与关键字具有相同的拼写。)您认为使用什么标准来决定哪些名称应该是关键字,哪些应该是预定义标识符?您是否同意这些选择?为什么或为什么不?

3.33 List the keywords (reserved words) of one or more programming languages. List the predefined identifiers. (Recall that every keyword is a separate token. An identifier cannot have the same spelling as a keyword.) What criteria do you think were used to decide which names should be keywords and which should be predefined identifiers? Do you agree with the choices? Why or why not?

3.34 如果你有使用 C、C++ 或 Rust 等语言的经验,其中动态分配的空间必须手动回收,请描述你在处理悬垂引用或内存泄漏方面的经验。这些情况发生的频率是多少错误出现了吗?你如何找到它们?需要付出多少努力?了解用于查找存储错误的开源或商业工具(Valgrind是一个流行的开源示例)。这些工具是否会削弱自动垃圾收集的论点?

3.34 If you have experience with a language like C, C++, or Rust, in which dynamically allocated space must be manually reclaimed, describe your experience with dangling references or memory leaks. How often do these bugs arise? How do you find them? How much effort does it take? Learn about open-source or commercial tools for finding storage bugs (Valgrind is a popular open-source example). Do such tools weaken the argument for automatic garbage collection?

3.35 一些语言(尤其是 Euclid 和 Turing)将每个子程序都设为封闭范围,并要求它显式导入它使用的任何非本地名称。导入列表可以被认为是子程序接口中通常隐式的部分的显式、强制文档。使用导入列表还使 Euclid 和 Turing 能够轻松禁止将变量通过引用传递给也直接访问该变量的子程序,从而避免示例 3.20中提到的错误。

在您编写的程序中,记录非本地变量的每次使用有多难?为提高文档质量和错误率而付出的努力是否值得?

3.35 A few languages—notably Euclid and Turing, make every subroutine a closed scope, and require it to explicitly import any nonlocal names it uses. The import lists can be thought of as explicit, mandatory documentation of a part of the subroutine interface that is usually implicit. The use of import lists also makes it easy for Euclid and Turing to prohibit passing a variable, by reference, to a subroutine that also accesses that variable directly, thereby avoiding the errors alluded to in Example 3.20.

In programs you have written, how hard would it have been to document every use of a nonlocal variable? Would the effort be worth the improvement in the quality of documentation and error rates?

3.36我们在 3.3.6 节中了解到,现代语言通常已经放弃了动态作用域。仍然可以在Unix 编程环境中的所谓环境变量中找到它。如果您不熟悉这些变量,请阅读您最喜欢的 shell(命令解释器— ksh/bashcsh/tcsh等)的手册页,以了解它们的行为方式。解释为什么动态作用域的常用替代方案(默认参数和静态变量)在这种情况下不合适。

3.36 We learned in Section 3.3.6 that modern languages have generally abandoned dynamic scoping. One place it can still be found is in the so-called environment variables of the Unix programming environment. If you are not familiar with these, read the manual page for your favorite shell (command interpreter—ksh/bash, csh/tcsh, etc.) to learn how these behave. Explain why the usual alternatives to dynamic scoping (default parameters and static variables) are not appropriate in this case.

3.37 比较 Ada 和 Modula-3 或 C# 中枚举名称的重载机制(第 3.5.2 节)。有人可能会认为(历史上较新的)Modula-3/C# 方法将责任从编译器转移到程序员:它要求即使明确使用枚举常量也要用其类型注释。您认为语言设计者为什么选择这种方法?您同意这个选择吗?为什么或为什么不?

3.37 Compare the mechanisms for overloading of enumeration names in Ada and in Modula-3 or C# (Section 3.5.2). One might argue that the (historically more recent) Modula-3/C# approach moves responsibility from the compiler to the programmer: it requires even an unambiguous use of an enumeration constant to be annotated with its type. Why do you think this approach was chosen by the language designers? Do you agree with the choice? Why or why not?

3.38 了解Perl 中的绑定变量。这些变量允许程序员将普通变量与(面向对象的)对象关联起来,这样对变量的操作就会自动解释为对对象的方法调用。例如,假设我们写了tie $my_var,“ my_class ”;。解释器将创建一个新的my_class类对象,并将其与标量变量 $ my_var关联。为了便于讨论,将该对象称为O 。现在,任何读取$my_var值的尝试都将被解释为对方法O -> FETCH()的调用。类似地,赋值$my_var = value将被解释为对O -> STORE ( value )的调用。数组、哈希和文件句柄变量支持一组更大的内置操作,在绑定时提供对一组更大的方法的访问。

将 Perl 的绑定机制与 C++ 的运算符重载进行比较。每种语言的哪些特性可以方便地被另一种语言模拟?

3.38 Learn about tied variables in Perl. These allow the programmer to associate an ordinary variable with an (object-oriented) object in such a way that operations on the variable are automatically interpreted as method invocations on the object. As an example, suppose we write tie $my_var, “my_class“;. The interpreter will create a new object of class my_class, which it will associate with scalar variable $my_var. For purposes of discussion, call that object O. Now, any attempt to read the value of $my_var will be interpreted as a call to method O->FETCH(). Similarly, the assignment $my_var = value will be interpreted as a call to O->STORE(value). Array, hash, and filehandle variables, which support a larger set of built-in operations, provide access to a larger set of methods when tied.

Compare Perl's tying mechanism to the operator overloading of C++. Which features of each language can be conveniently emulated by the other?

3.39 你认为强制措施是个好主意吗?为什么?

3.39 Do you think coercion is a good idea? Why or why not?

3.40  Ruby 中 lambda 表达式的语法随着时间的推移而不断发展,现在有四种方法可以将作为闭包传递到方法中:将其放在参数列表末尾(在这种情况下,它将成为额外的最终参数);将其传递给Proc.new;或者,在参数列表中,在其前面加上关键字 lambda 或将其写入 -> lambda 符号。研究这些选项。哪个先出现?哪个后来出现?它们的比较优势是什么?它们的行为是否存在细微差别?

3.40 The syntax for lambda expressions in Ruby evolved over time, with the result that there are now four ways to pass a block into a method as a closure: by placing it after the end of the argument list (in which case it become an extra, final parameter); by passing it to Proc.new; or, within the argument list, by prefixing it with the keyword lambda or by writing it in -> lambda notation. Investigate these options. Which came first? Which came later? What are their comparative advantages? Are their any minor differences in their behavior?

3.41  Lambda 表达式是 Java 编程语言的后期添加的:多年来一直受到强烈抵制。研究围绕它们的争议。你支持哪一种?哪些替代方案被拒绝了?你觉得其中有什么有吸引力吗?

3.41 Lambda expressions were a late addition to the Java programming language: they were strongly resisted for many years. Research the controversy surrounding them. Where do your sympathies lie? What alternative proposals were rejected? Do you find any of them appealing?

3.42 举三个例子,说明你熟悉的语言没有提供哪些功能,但在其他语言中却很常见。你认为缺少这些功能的原因是什么?它们会使语言的实现复杂化吗?如果是这样,这种复杂化(你认为)是否合理?

3.42 Give three examples of features that are not provided in some language with which you are familiar, but that are common in other languages. Why do you think these features are missing? Would they complicate the implementation of the language? If so, would the complication (in your judgment) be justified?

03-02-9780124104099 3.43–3.47  更深入。

3.43–3.47  In More Depth.

3.12 书目注释

3.12 Bibliographic Notes

本章追溯了多种语言中命名和作用域机制的演变,包括 Fortran(多个版本)、Basic、Algol 60 和 68、Pascal、Simula、C 和 C++、Euclid、Turing、Modula(1、2 和 3)、Ada(83 和 95)、Oberon、Eiffel、Perl、Tcl、Python、Ruby、Rust、Java 和 C#。所有这些书目的参考资料都可以在附录 A中找到。

This chapter has traced the evolution of naming and scoping mechanisms through a very large number of languages, including Fortran (several versions), Basic, Algol 60 and 68, Pascal, Simula, C and C++, Euclid, Turing, Modula (1, 2, and 3), Ada (83 and 95), Oberon, Eiffel, Perl, Tcl, Python, Ruby, Rust, Java, and C#. Bibliographic references for all of these can be found in Appendix A.

模块和对象都起源于 Simula,它是由 Dahl、Nygaard、Myhrhaug 等人在 20 世纪 60 年代中期在挪威计算中心开发的。(Simula I 于 1964 年实现;本书中的描述涉及 Simula 67。)Clu、Modula、Euclid 和相关语言的开发人员在 20 世纪 70 年代改进了 Simula 的封装机制。Simula 的其他创新(尤其是继承和动态方法绑定)为 Smalltalk 提供了灵感,Smalltalk 是面向对象语言的最初版本,可以说是最纯粹的。现代面向对象语言(包括 Eiffel、C++、Java、C#、Python 和 Ruby)在很大程度上代表了封装与继承和动态方法绑定演进路线的重新整合。

Both modules and objects trace their roots to Simula, which was developed by Dahl, Nygaard, Myhrhaug, and others at the Norwegian Computing Center in the mid-1960s. (Simula I was implemented in 1964; descriptions in this book pertain to Simula 67.) The encapsulation mechanisms of Simula were refined in the 1970s by the developers of Clu, Modula, Euclid, and related languages. Other Simula innovations—inheritance and dynamic method binding in particular—provided the inspiration for Smalltalk, the original and arguably purest of the object-oriented languages. Modern object-oriented languages, including Eiffel, C++, Java, C#, Python, and Ruby, represent to a large extent a reintegration of the evolutionary lines of encapsulation on the one hand and inheritance and dynamic method binding on the other.

信息隐藏的概念起源于 Parnas 的经典论文“关于将系统分解为模块所用的标准” [ Par72 ]。关于命名、作用域和抽象机制的比较讨论可以在以下地方找到:Liskov 等人对 Clu 的讨论 [ LSAS77 ],Liskov 和 Guttag 的文本 [ LG86第 4 章],Ada 基本原理 [ IBFW91第 912章],Harbison 关于 Modula-3 的文本 [ Har92第 89章],Wirth 早期关于模块的工作 [ Wir80 ],以及他后来对 Modula 和 Oberon 的讨论 [ Wir88aWir07 ]。关于面向对象语言的更多信息,请参见第 10 章

The notion of information hiding originates in Parnas's classic paper, “On the Criteria to be Used in Decomposing Systems into Modules” [Par72]. Comparative discussions of naming, scoping, and abstraction mechanisms can be found, among other places, in Liskov et al.'s discussion of Clu [LSAS77], Liskov and Guttag's text [LG86, Chap. 4], the Ada Rationale [IBFW91, Chaps. 912], Harbison's text on Modula-3 [Har92, Chaps. 89], Wirth's early work on modules [Wir80], and his later discussion of Modula and Oberon [Wir88a, Wir07]. Further information on object-oriented languages can be found in Chapter 10.

有关重载和多态性的详细讨论,请参阅 Cardelli 和 Wegner 的综述 [ CW85 ]。Cailliau [ Cai82 ] 对第 3.3.3 节中指出的许多范围界定陷阱进行了轻松的讨论。Abelson 和 Sussman [ AS96,第 11n 页] 将“语法糖”一词归功于 Peter Landin。

For a detailed discussion of overloading and polymorphism, see the survey by Cardelli and Wegner [CW85]. Cailliau [Cai82] provides a lighthearted discussion of many of the scoping pitfalls noted in Section 3.3.3. Abelson and Sussman [AS96, p. 11n] attribute the term “syntactic sugar” to Peter Landin.

Jarvi 和 Freeman 的论文 [ JF10 ] 描述了 C++ 的 Lambda 表达式。Java 的 Lambda 表达式是在 Java Community Process 的 JSR 335 下开发的(文档位于jcp.org)。

Lambda expressions for C++ are described in the paper of Jarvi and Freeman [JF10]. Lambda expressions for Java were developed under JSR 335 of the Java Community Process (documentation at jcp.org).


1由于缺乏更好的术语,我们将在第 3 章至第9章中使用术语“对象”来指代可能有名称的任何事物:变量、常量、类型、子例程、模块等。在许多现代语言中,“对象”具有更正式的含义,我们将在第 10 章中讨论。

1 For want of a better term, we will use the term “object” throughout Chapters 3–9 to refer to anything that might have a name: variables, constants, types, subroutines, modules, and others. In many modern languages “object” has a more formal meaning, which we will consider in Chapter 10.

2不幸的是,“堆”这个术语也用于基于树的优先级队列的常见实现。这两个术语的用法彼此无关。

2 Unfortunately, the term “heap” is also used for the common tree-based implementation of a priority queue. These two uses of the term have nothing to do with one another.

3 词法作用域实际上是一个比静态作用域更好的术语,因为基于嵌套的作用域规则可以在运行时而不是编译时强制执行(如果需要)。实际上,在 Common Lisp 和 Scheme 中,可以将子例程声明的未求值文本作为参数传递给其他子例程,然后使用该文本在运行时创建词法嵌套声明。

3 Lexical scope is actually a better term than static scope, because scope rules based on nesting can be enforced at run time instead of compile time if desired. In fact, in Common Lisp and Scheme it is possible to pass the unevaluated text of a subroutine declaration into some other subroutine as a parameter, and then use the text to create a lexically nested declaration at run time.

4该代码并非人为设计的;它是从第 C-2.3.5 节中描述的 FMQ 错误修复算法的实现(最初以 Pascal 编写)中提取出来的。

4 This code is not contrived; it was extracted from an implementation (originally in Pascal) of the FMQ error repair algorithm described in Section C-2.3.5.

5 C++ :: 运算符还用于命名被派生类成员隐藏的基类成员(字段或方法);我们将在第 10.2.2 节中考虑这种用法。

5 The C++ :: operator is also used to name members (fields or methods) of a base class that are hidden by members of a derived class; we will consider this use in Section 10.2.2.

6我们在2.3.1 节的递归下降解析中看到了一个相互递归子程序的例子。相互递归类型经常出现在链接数据结构中,其中两种类型的节点可能需要指向彼此。

6 We saw an example of mutually recursive subroutines in the recursive descent parsing of Section 2.3.1. Mutually recursive types frequently arise in linked data structures, where nodes of two types may need to point to each other.

7 Barbara Liskov (1939-),Clu 的首席设计师,是抽象机制历史上的领军人物之一。她自 1971 年起担任麻省理工学院的教员,也是 Argus 编程语言的首席设计师,该语言结合了语言和数据库技术,以提高分布式系统的可靠性和可编程性。她于 2008 年获得 ACM 图灵奖。

7 Barbara Liskov (1939–), the principal designer of Clu, is one of the leading figures in the history of abstraction mechanisms. A faculty member at MIT since 1971, she was also the principal designer of the Argus programming language, which combined language and database technology to improve the reliability and programmability of distributed systems. She received the ACM Turing Award in 2008.

8少数语言(尤其是 ML 系列的成员)具有带继承的模块类型,但仍没有动态方法分派。大多数语言中的模块都缺少这两个功能。

8 A few languages—notably members of the ML family—have module types with inheritance—but still without dynamic method dispatch. Modules in most languages are missing both features.

9 Scheme 和 Common Lisp 都是静态作用域,尽管后者允许程序员为单个变量指定动态作用域。静态作用域是在版本 5 中添加到 Perl 的;程序员现在可以在每个变量声明中明确选择静态或动态作用域。(我们将在第 14.4.1 节中更详细地讨论这一选择。)

9 Scheme and Common Lisp are statically scoped, though the latter allows the programmer to specify dynamic scoping for individual variables. Static scoping was added to Perl in version 5; the programmer now chooses static or dynamic scoping explicitly in each variable declaration. (We consider this choice in more detail in Section 14.4.1.)

10一些作者认为,一流地位需要匿名函数定义(lambda 表达式),这些定义可以嵌入到其他表达式中。C#、几种脚本语言和所有函数式语言都满足此要求,但许多命令式语言却不满足。

10 Some authors would say that first-class status requires anonymous function definitions—lambda expressions—that can be embedded in other expressions. C#, several scripting languages, and all functional languages meet this requirement, but many imperative languages do not.

4

语义分析

Semantic Analysis

第 2 章中 我们讨论了编程语言语法这一主题。在本章中,我们将讨论语义这一主题。非正式地说,语法涉及有效程序的形式,而语义涉及其含义。含义之所以重要,至少有两个原因:它使我们能够执行超越单纯形式的规则(例如,类型一致性),并且它提供了我们生成等效输出程序所需的信息。

In Chapter 2 we considered the topic of programming language syntax. In the current chapter we turn to the topic of semantics. Informally, syntax concerns the form of a valid program, while semantics concerns its meaning. Meaning is important for at least two reasons: it allows us to enforce rules (e.g., type consistency) that go beyond mere form, and it provides the information we need in order to generate an equivalent output program.

通常来说,语言的语法就是语言定义中可以方便地用上下文无关文法描述的部分,而语义则是定义中不能方便地描述的部分。这种惯例在实践中很有用,尽管它并不总是与直觉一致。例如,当我们要求子程序调用中包含的参数数量与子程序定义中的形式参数数量相匹配时,很容易说这个要求是语法问题。毕竟,我们可以计算参数而不知道它们的含义。不幸的是,我们无法使用上下文无关规则来计算它们。同样,虽然可以编写一个上下文无关文法,其中每个函数都必须包含至少一个返回语句,但所需的复杂性使这种策略非常没有吸引力。一般来说,任何要求编译器比较相距很远的事物或计算未正确嵌套的事物的规则最终都是语义问题。

It is conventional to say that the syntax of a language is precisely that portion of the language definition that can be described conveniently by a context-free grammar, while the semantics is that portion of the definition that cannot. This convention is useful in practice, though it does not always agree with intuition. When we require, for example, that the number of arguments contained in a call to a subroutine match the number of formal parameters in the subroutine definition, it is tempting to say that this requirement is a matter of syntax. After all, we can count arguments without knowing what they mean. Unfortunately, we cannot count them with context-free rules. Similarly, while it is possible to write a context-free grammar in which every function must contain at least one return statement, the required complexity makes this strategy very unattractive. In general, any rule that requires the compiler to compare things that are separated by long distances, or to count things that are not properly nested, ends up being a matter of semantics.

语义规则进一步分为静态语义和动态语义,尽管两者之间的界限仍然有些模糊。编译器在编译时强制执行静态语义规则。它生成代码以在运行时强制执行动态语义规则(或调用执行此操作的库例程)。某些错误(例如除以零或尝试使用越界下标索引数组)通常无法在编译时捕获,因为它们可能仅针对某些输入值或任意复杂代码的某些行为发生。在特殊情况下,编译器可能能够判断某个错误将始终发生或永远不会发生,而与运行时输入无关。在这些情况下,编译器可以在编译时生成错误消息,或者根据需要避免生成代码以在运行时执行检查。然而,可计算性理论的基本结果告诉我们,没有算法可以对任意程序正确做出这些预测:不可避免地会出现这样的情况:错误总是会发生,但编译器无法分辨,必须将错误消息延迟到运行时;也会出现这样的情况:错误永远不会发生,但编译器无法分辨,必须承担不必要的运行时检查的成本。

Semantic rules are further divided into static and dynamic semantics, though again the line between the two is somewhat fuzzy. The compiler enforces static semantic rules at compile time. It generates code to enforce dynamic semantic rules at run time (or to call library routines that do so). Certain errors, such as division by zero, or attempting to index into an array with an out-of-bounds subscript, cannot in general be caught at compile time, since they may occur only for certain input values, or certain behaviors of arbitrarily complex code. In special cases, a compiler maybe able to tell that a certain error will always or never occur, regardless of run-time input. In these cases, the compiler can generate an error message at compile time, or refrain from generating code to perform the check at run time, as appropriate. Basic results from computability theory, however, tell us that no algorithm can make these predictions correctly for arbitrary programs: there will inevitably be cases in which an error will always occur, but the compiler cannot tell, and must delay the error message until run time; there will also be cases in which an error can never occur, but the compiler cannot tell, and must incur the cost of unnecessary run-time checks.

语义分析和中间代码生成都可以用注释或解析树或语法树的修饰来描述。注释本身称为属性。后续章节将出现大量静态和动态语义规则的示例。在本章中,我们主要关注编译器用来执行静态规则的机制。我们将在第 15 章中讨论中间代码生成(包括生成用于动态语义检查的代码)。

Both semantic analysis and intermediate code generation can be described in terms of annotation, or decoration of a parse tree or syntax tree. The annotations themselves are known as attributes. Numerous examples of static and dynamic semantic rules will appear in subsequent chapters. In this current chapter we focus primarily on the mechanisms a compiler uses to enforce the static rules. We will consider intermediate code generation (including the generation of code for dynamic semantic checks) in Chapter 15.

第 4.1 节中,我们将更详细地考虑语义分析器的作用,考虑它需要执行的规则以及它与其他编译阶段的关系。本章的其余部分将专门讨论属性语法这一主题。属性语法为树的修饰提供了一个正式的框架。即使在那些不将解析树或语法树构建为显式数据结构的编译器中,这个框架也是一个有用的概念工具。我们在第 4.2 节中介绍了属性语法的概念。然后,我们考虑了在实践中应用此类语法的各种方式。第 4.3 节讨论了属性流的问题,它限制了树节点的修饰顺序。在实践中,大多数编译器要求在 LL 或 LR 解析过程中修饰解析树(或评估解析树中存在的属性(如果有))。第 4.4 节介绍了动作例程,作为此类“即时”评估的临时机制。在第 4.5 节(主要在配套网站上)中,我们考虑了解析树属性的空间管理。

In Section 4.1 we consider the role of the semantic analyzer in more detail, considering both the rules it needs to enforce and its relationship to other phases of compilation. Most of the rest of the chapter is then devoted to the subject of attribute grammars. Attribute grammars provide a formal framework for the decoration of a tree. This framework is a useful conceptual tool even in compilers that do not build a parse tree or syntax tree as an explicit data structure. We introduce the notion of an attribute grammar in Section 4.2. We then consider various ways in which such grammars can be applied in practice. Section 4.3 discusses the issue of attribute flow, which constrains the order(s) in which nodes of a tree can be decorated. In practice, most compilers require decoration of the parse tree (or the evaluation of attributes that would reside in a parse tree if there were one) to occur in the process of an LL or LR parse. Section 4.4 presents action routines as an ad hoc mechanism for such “on-the-fly” evaluation. In Section 4.5 (mostly on the companion site) we consider the management of space for parse tree attributes.

由于解析树必须反映 CFG 的结构,因此解析树往往非常复杂(回想一下图 1.5中的示例)。解析完成后,我们通常希望用更直接地反映输入程序的语法树替换解析树(图 1.6 )。一个特别常见的编译器组织在解析过程中使用动作例程,其目的仅仅是构建语法树。然后在单独的遍历过程中修饰语法树,如果需要,可以使用单独的属性语法将其形式化。我们将在第 4.6 节中考虑语法树的修饰。

Because they have to reflect the structure of the CFG, parse trees tend to be very complicated (recall the example in Figure 1.5). Once parsing is complete, we typically want to replace the parse tree with a syntax tree that reflects the input program in a more straightforward way (Figure 1.6). One particularly common compiler organization uses action routines during parsing solely for the purpose of constructing the syntax tree. The syntax tree is then decorated during a separate traversal, which can be formalized, if desired, with a separate attribute grammar. We consider the decoration of syntax trees in Section 4.6.

4.1 语义分析器的作用

4.1 The Role of the Semantic Analyzer

编程语言在语义规则的选择上差异巨大。例如,Lisp 方言允许对任意数字类型进行“混合模式”运算,它们将根据需要自动将其从整数提升为有理数,再提升为浮点数或“大数”(扩展)精度,以保持精度。而 Ada 则按照惯例为每个数字变量分配一个特定类型,并要求程序员在表达式中组合它们时明确地在这些变量之间进行转换。不同语言在要求其实现执行动态检查的程度上也有所不同。在一个极端,C 根本不需要检查,除了硬件附带的那些“免费”检查(例如,除以零,或试图访问程序边界之外的内存)。在另一个极端,Java 不遗余力地检查尽可能多的规则,部分原因是为了确保不受信任的程序不会做任何事情来损坏它所运行的机器的内存或文件。语义分析器的作用是强制执行所有静态语义规则并使用中间代码生成器所需的信息注释程序。这些信息包括澄清(这是浮点加法,而不是整数;这是对全局变量x的引用)和动态语义检查的要求。

Programming languages vary dramatically in their choice of semantic rules. Lisp dialects, for example, allow “mixed-mode” arithmetic on arbitrary numeric types, which they will automatically promote from integer to rational to floating-point or “bignum” (extended) precision, as required to maintain precision. Ada, by contract, assigns a specific type to every numeric variable, and requires the programmer to convert among these explicitly when combining them in expressions. Languages also vary in the extent to which they require their implementations to perform dynamic checks. At one extreme, C requires no checks at all, beyond those that come “free” with the hardware (e.g., division by zero, or attempted access to memory outside the bounds of the program). At the other extreme, Java takes great pains to check as many rules as possible, in part to ensure that an untrusted program cannot do anything to damage the memory or files of the machine on which it runs. The role of the semantic analyzer is to enforce all static semantic rules and to annotate the program with information needed by the intermediate code generator. This information includes both clarifications (this is floating-point addition, not integer; this is a reference to the global variable x) and requirements for dynamic semantic checks.

在典型的编译器中,分析和中间代码生成标志着前端计算的结束。然而,前端和后端之间的确切分工可能因编译器而异:很难确切地说分析(弄清楚程序的含义)在哪里结束,综合(以某种新形式表达该含义)在哪里开始(并且如第1.6 节所述,两者之间可能存在一个“中间端”)。许多编译器还使程序经历多种中间形式。在一种常见的组织中(第15 章将更详细地描述),语义分析器创建带注释的语法树,然后中间代码生成器将其转换为线性形式,让人联想到某些理想机器的汇编语言。在独立于机器的代码改进之后,这种线性形式被转换成另一种形式,更紧密地模仿目标机器的汇编语言。该形式可能会进行特定于机器的代码改进。

In the typical compiler, analysis and intermediate code generation mark the end of front end computation. The exact division of labor between the front end and the back end, however, may vary from compiler to compiler: it can be hard to say exactly where analysis (figuring out what the program means) ends and synthesis (expressing that meaning in some new form) begins (and as noted in Section 1.6 there maybe a “middle end” in between). Many compilers also carry a program through more than one intermediate form. In one common organization, described in more detail in Chapter 15, the semantic analyzer creates an annotated syntax tree, which the intermediate code generator then translates into a linear form reminiscent of the assembly language for some idealized machine. After machine-independent code improvement, this linear form is then translated into yet another form, patterned more closely on the assembly language of the target machine. That form may undergo machine-specific code improvement.

编译器在语义分析和中间代码生成与解析交错的程度上也有所不同。在完全分离的阶段中,解析器将完整的解析树传递给语义分析器,语义分析器将其转换为语法树,填充符号表,执行语义检查,并将其传递给代码生成器。在完全交错的阶段中,可能不需要构建整个解析树或语法树:解析器可以在解析源代码的每个表达式、语句或子例程时动态调用语义检查和代码生成例程。我们将重点介绍一种组织,其中语法树的构建与解析交错(并且不构建解析树),但语义分析发生在单独的语法树遍历期间。

Compilers also vary in the extent to which semantic analysis and intermediate code generation are interleaved with parsing. With fully separated phases, the parser passes a full parse tree on to the semantic analyzer, which converts it to a syntax tree, fills in the symbol table, performs semantic checks, and passes it on to the code generator. With fully interleaved phases, there may be no need to build either the parse tree or the syntax tree in its entirety: the parser can call semantic check and code generation routines on the fly as it parses each expression, statement, or subroutine of the source. We will focus on an organization in which construction of the syntax tree is interleaved with parsing (and the parse tree is not built), but semantic analysis occurs during a separate traversal of the syntax tree.

动态检查

Dynamic Checks

许多生成动态检查代码的编译器都提供了禁用动态检查的选项(如果需要)。一些组织习惯在程序开发和测试期间启用动态检查,然后在生产使用时禁用它们,以提高执行速度。这种做法的合理性值得怀疑:编程语言设计领域的关键人物之一 Tony Hoare1将禁用语义检查的程序员比作帆船爱好者,他在陆地上训练时穿着救生衣,但出海时却脱掉它 [ Hoa89,第 198 页]。在生产使用中出错的可能性可能比在测试中要小,但未检测到的错误的后果要严重得多。此外,在现代处理器上,动态检查通常可以在原本未使用的流水线槽中执行,从而使这些槽几乎不花费任何成本。另一方面,一些动态检查(例如,确保 C 语言中的指针算法保持在数组范围内)非常昂贵,因此很少实现。

Many compilers that generate code for dynamic checks provide the option of disabling them if desired. It is customary in some organizations to enable dynamic checks during program development and testing, and then disable them for production use, to increase execution speed. The wisdom of this practice is questionable: Tony Hoare, one of the key figures in programming language design,1 has likened the programmer who disables semantic checks to a sailing enthusiast who wears a life jacket when training on dry land, but removes it when going to sea [Hoa89, p. 198]. Errors may be less likely in production use than they are in testing, but the consequences of an undetected error are significantly worse. Moreover, on modern processors it is often possible for dynamic checks to execute in pipeline slots that would otherwise go unused, making them virtually free. On the other hand, some dynamic checks (e.g., ensuring that pointer arithmetic in C remains within the bounds of an array) are sufficiently expensive that they are rarely implemented.

断言

Assertions

例 4.1

Example 4.1

Java 中的断言

Assertions in Java

在推理算法的正确性时(或通过公理语义正式证明程序的属性时),程序员经常会编写有关程序数据值的逻辑断言。一些编程语言将这些断言作为语言语法的一部分。然后,编译器会生成代码来在运行时检查断言。断言是一种语句,当执行到代码中的某个点时,指定的条件预计为真。在 Java 中,可以编写

When reasoning about the correctness of their algorithms (or when formally proving properties of programs via axiomatic semantics) programmers frequently write logical assertions regarding the values of program data. Some programming languages make these assertions a part of the language syntax. The compiler then generates code to check the assertions at run time. An assertion is a statement that a specified condition is expected to be true when execution reaches a certain point in the code. In Java one can write

设计与实现

Design & Implementation

4.1 动态语义检查

4.1 Dynamic semantic checks

过去,语言理论家和编程方法论及软件工程的研究人员倾向于主张更广泛的语义检查,而“现实世界”的程序员则“用脚投票”支持 C 和 Fortran 等语言,这些语言为了提高执行速度而省略了这些检查。随着计算机功能越来越强大,公司也开始意识到软件维护的巨大成本,“现实世界”阵营对检查的态度也越来越积极。Ada 和 Java 等语言从一开始就考虑到了安全性,而 C 和 C++ 等语言则(在可能的范围内)朝着越来越严格的定义发展。在脚本语言中,许多语义检查被推迟到运行时,以避免需要显式类型和变量声明,因此也有类似的趋向于制定更严格的规则。例如,Perl(一种较老的脚本语言)通常会尝试推断表达式(例如3 + “four”)的可能含义,而较新的语言(例如 Python 或 Ruby)会将其标记为运行时错误。

In the past, language theorists and researchers in programming methodology and software engineering tended to argue for more extensive semantic checks, while “real-world” programmers “voted with their feet” for languages like C and Fortran, which omitted those checks in the interest of execution speed. As computers have become more powerful, and as companies have come to appreciate the enormous costs of software maintenance, the “real-world” camp has become much more sympathetic to checking. Languages like Ada and Java have been designed from the outset with safety in mind, and languages like C and C++ have evolved (to the extent possible) toward increasingly strict definitions. In scripting languages, where many semantic checks are deferred until run time in order to avoid the need for explicit types and variable declarations, there has been a similar trend toward stricter rules. Perl, for example (one of the older scripting languages), will typically attempt to infer a possible meaning for expressions (e.g., 3 + “four”) that newer languages (e.g., Python or Ruby) will flag as run-time errors.

断言分母!= 0;

assert denominator != 0;

如果运行时语义检查失败,则会抛出AssertionError异常。

An AssertionError exception will be thrown if the semantic check fails at run time. ■

某些语言(例如 Euclid、Eiffel 和 Ada 2012)还明确支持不变量、先决条件后置条件。这些本质上是结构化断言。不变量在给定代码体的所有“干净点”上都应该为真。在 Eiffel 中,程序员可以在类内的数据上指定一个不变量:该不变量将在该类的每个方法(子程序)的开始和结束时自动进行检查。循环的类似不变量在每次迭代之前和之后都应该为真。先决条件和后置条件应该分别在子程序的开始和结束时为真。在 Euclid 中,在子程序头部指定一次的后置条件不仅会在子程序文本的末尾进行检查,还会在每个返回语句处进行检查。

Some languages (e.g., Euclid, Eiffel, and Ada 2012) also provide explicit support for invariants, preconditions, and postconditions. These are essentially structured assertions. An invariant is expected to be true at all “clean points” of a given body of code. In Eiffel, the programmer can specify an invariant on the data inside a class: the invariant will be checked, automatically, at the beginning and end of each of the class's methods (subroutines). Similar invariants for loops are expected to be true before and after every iteration. Pre- and postconditions are expected to be true at the beginning and end of subroutines, respectively. In Euclid, a postcondition, specified once in the header of a subroutine, will be checked not only at the end of the subroutine's text, but at every return statement as well.

例 4.2

Example 4.2

C 中的断言

Assertions in C

许多语言通过标准库例程或宏支持断言。例如,在 C 中,可以这样写

Many languages support assertions via standard library routines or macros. In C, for example, one can write

断言(分母!= 0);

assert(denominator != 0);

如果断言失败,程序将突然终止并显示以下消息

If the assertion fails, the program will terminate abruptly with the message

myprog.c:42:断言‘分母 != 0’失败

myprog.c:42: failed assertion 'denominator != 0'

C 手册要求将 assert 实现为宏(或内置于编译器中),以便它可以访问其参数的文本表示形式以及出现调用的文件名和行号。■

The C manual requires assert to be implemented as a macro (or built into the compiler) so that it has access to the textual representation of its argument, and to the file name and line number on which the call appears. ■

当然,断言可用于覆盖其他三种检查,但不那么清晰或简洁。不变量、先决条件和后置条件是它们适用的代码头中的重要部分,并且可以覆盖大量原本需要断言的地方。Euclid 和 Eiffel 实现允许程序员在需要时禁用断言和相关构造,以消除它们的运行时成本。

Assertions, of course, could be used to cover the other three sorts of checks, but not as clearly or succinctly. Invariants, preconditions, and postconditions are a prominent part of the header of the code to which they apply, and can cover a potentially large number of places where an assertion would otherwise be required. Euclid and Eiffel implementations allow the programmer to disable assertions and related constructs when desired, to eliminate their run-time cost.

静态分析

Static Analysis

一般而言,预测运行时行为的编译时算法称为静态分析。如果这种分析允许编译器确定给定程序是否始终遵循规则,则称其为精确分析。例如,在 Ada 和 ML 等语言中,类型检查是静态且精确的:编译器确保在运行时不会以不适合其类型的方式使用变量。相比之下,Lisp、Smalltalk、Python 和 Ruby 等语言通过接受动态类型检查的运行时开销,获得了更大的灵活性,同时保持了完全类型安全。(我们将在第 7 章中更详细地介绍类型检查。)

In general, compile-time algorithms that predict run-time behavior are known as static analysis. Such analysis is said to be precise if it allows the compiler to determine whether a given program will always follow the rules. Type checking, for example, is static and precise in languages like Ada and ML: the compiler ensures that no variable will ever be used at run time in a way that is inappropriate for its type. By contrast, languages like Lisp, Smalltalk, Python, and Ruby obtain greater flexibility, while remaining completely type-safe, by accepting the runtime overhead of dynamic type checks. (We will cover type checking in more detail in Chapter 7.)

静态分析在不精确的情况下也很有用。编译器通常会在编译时检查它们能检查的内容,然后生成代码来动态检查其余部分。例如,在 Java 中,类型检查主要是静态的,但动态加载的类和类型转换可能需要运行时检查。同样,许多编译器执行广泛的静态分析,试图消除对数组下标、变体记录标签或潜在悬垂指针(将在第 8 章中讨论)进行动态检查的需要。

Static analysis can also be useful when it isn't precise. Compilers will often check what they can at compile time and then generate code to check the rest dynamically. In Java, for example, type checking is mostly static, but dynamically loaded classes and type casts may require run-time checks. In a similar vein, many compilers perform extensive static analysis in an attempt to eliminate the need for dynamic checks on array subscripts, variant record tags, or potentially dangling pointers (to be discussed in Chapter 8).

如果我们将省略不必要的动态检查视为一种性能优化,那么寻找静态分析可以改进代码的其他方式也是很自然的。我们将在第17 章中更详细地讨论这个主题。例子包括别名分析,它决定何时可以将值安全地缓存在寄存器中、以“无序”计算或由并发线程访问;逃逸分析,它决定何时将对值的所有引用限制在给定的上下文中,从而允许在堆栈而不是堆上分配值,或者无需锁即可访问;以及子类型分析,它决定何时保证面向对象语言中的变量具有某种子类型,以便可以调用其方法而无需动态分派。

If we think of the omission of unnecessary dynamic checks as a performance optimization, it is natural to look for other ways in which static analysis may enable code improvement. We will consider this topic in more detail in Chapter 17. Examples include alias analysis, which determines when values can be safely cached in registers, computed “out of order,” or accessed by concurrent threads; escape analysis, which determines when all references to a value will be confined to a given context, allowing the value to be allocated on the stack instead of the heap, or to be accessed without locks; and subtype analysis, which determines when a variable in an object-oriented language is guaranteed to have a certain subtype, so that its methods can be called without dynamic dispatch.

如果某种优化可能导致某些程序的代码不正确,则称其为不安全的优化。如果它通常可以提高性能,但在某些情况下会降低性能,则称其为推测性的。如果编译器只有在能够保证优化既安全又有效时才应用优化,则称其为保守性的。相反,乐观的编译器可能会大量使用推测性优化。它还可以通过生成两个版本的代码来进行不安全的优化,并根据编译时不可用的信息进行动态检查以在两个版本之间进行选择。推测性优化的例子包括非绑定预取(它试图在需要数据之前将其放入缓存中)和跟踪调度(它重新排列代码以期提高处理器管道和指令缓存的性能)。

An optimization is said to be unsafe if it may lead to incorrect code in certain programs. It is said to be speculative if it usually improves performance, but may degrade it in certain cases. A compiler is said to be conservative if it applies optimizations only when it can guarantee that they will be both safe and effective. By contrast, an optimistic compiler may make liberal use of speculative optimizations. It may also pursue unsafe optimizations by generating two versions of the code, with a dynamic check that chooses between them based on information not available at compile time. Examples of speculative optimization include nonbinding prefetches, which try to bring data into the cache before they are needed, and trace scheduling, which rearranges code in hopes of improving the performance of the processor pipeline and the instruction cache.

为了消除动态检查,语言设计者可能会选择收紧语义规则,禁止保守分析失败的程序。例如,ML 类型系统(第 7.2.4 节)避免了 Lisp 的动态类型检查,但不允许 Lisp 支持的某些有用的编程习惯用法。类似地,Java 和 C# 的明确赋值规则(第 6.1.3 节)允许编译器确保变量在表达式中使用之前始终被赋值,但不允许某些在 C 中合法(且正确)的程序。

To eliminate dynamic checks, language designers may choose to tighten semantic rules, banning programs for which conservative analysis fails. The ML type system, for example (Section 7.2.4), avoids the dynamic type checks of Lisp, but disallows certain useful programming idioms that Lisp supports. Similarly, the definite assignment rules of Java and C# (Section 6.1.3) allow the compiler to ensure that a variable is always given a value before it is used in an expression, but disallow certain programs that are legal (and correct) in C.

4.2 属性文法

4.2 Attribute Grammars

例 4.3

Example 4.3

自下而上的常量表达式 CFG

Bottom-up CFG for constant expressions

第 2 章中,我们学习了如何使用上下文无关文法来指定编程语言的语法。例如,下面是一个由常量组成的算术表达式的 LR(自下而上)文法,具有优先级和结合性:2

In Chapter 2 we learned how to use a context-free grammar to specify the syntax of a programming language. Here, for example, is an LR (bottom-up) grammar for arithmetic expressions composed of constants, with precedence and associativity:2

+

EE + T

EET

EET

ET

TT * F

TT * F

TT / F

时间时间

TF

F−F

F → − F

→ (

F → ( E )

F → 常量

F → const

例 4.4

Example 4.4

自下而上的 AG 用于常量表达式

Bottom-up AG for constant expressions

该语法将生成所有基于基本算术运算符的正确格式的常量表达式,但它没有说明它们的含义。为了将这些表达式与数学概念(而不是地板瓷砖图案或舞步)联系起来,我们需要额外的符号。最常见的是基于属性的。在我们的表达式语法中,我们可以将val属性与语法中的每个ETF和 const 关联起来。目的是对于任何符号SS.val将是从S派生的标记字符串的含义(作为算术值)。我们假设扫描器为我们提供了constval。然后,我们必须为每个产生式设计一组规则,以指定不同符号的val之间的关系。得到的属性语法(AG)如图4.1所示。

This grammar will generate all properly formed constant expressions over the basic arithmetic operators, but it says nothing about their meaning. To tie these expressions to mathematical concepts (as opposed to, say, floor tile patterns or dance steps), we need additional notation. The most common is based on attributes. In our expression grammar, we can associate a val attribute with each E, T, F, and const in the grammar. The intent is that for any symbol S, S.val will be the meaning, as an arithmetic value, of the token string derived from S. We assume that the val of a const is provided to us by the scanner. We must then invent a set of rules for each production, to specify how the vals of different symbols are related. The resulting attribute grammar (AG) is shown in Figure 4.1.

编号04-01-9780124104099
图 4.1 常量表达式的简单属性文法,使用标准算术运算。每条语义规则都由一个符号引入图片

在这个简单的语法中,每个产生式都有一条规则。稍后我们将看到更复杂的语法,其中产生式可以有几条规则。规则有两种形式。产生式 3、6、8 和 9 中的规则称为复制规则;它们指定一个属性应该是另一个属性的副本。其他规则调用语义函数sum、quotient、additive_inverse等)。在这个例子中,语义函数都是我们熟悉的算术运算。一般来说,它们可以是语言设计者指定的任意复杂的函数。每个语义函数都接受任意数量的参数(每个参数必须是当前产生式中符号的属性——不允许使用全局变量),并且每个语义函数都计算单个结果,该结果同样必须分配给当前产生式中符号的属性。当一个产生式中有多个符号具有相同的名称时,使用下标来区分它们。这些下标仅用于语义函数;它们不是上下文无关语法本身的一部分。■

In this simple grammar, every production has a single rule. We shall see more complicated grammars later, in which productions can have several rules. The rules come in two forms. Those in productions 3, 6, 8, and 9 are known as copy rules; they specify that one attribute should be a copy of another. The other rules invoke semantic functions (sum, quotient, additive_inverse, etc.). In this example, the semantic functions are all familiar arithmetic operations. In general, they can be arbitrarily complex functions specified by the language designer. Each semantic function takes an arbitrary number of arguments (each of which must be an attribute of a symbol in the current production—no global variables are allowed), and each computes a single result, which must likewise be assigned into an attribute of a symbol in the current production. When more than one symbol of a production has the same name, subscripts are used to distinguish them. These subscripts are solely for the benefit of the semantic functions; they are not part of the context-free grammar itself. ■

例 4.5

Example 4.5

自上而下 AG 计算列表元素数量

Top-down AG to count the elements of a list

在属性文法的严格定义中,复制规则和语义函数调用是仅有的两种允许的规则。在我们的示例中,我们使用符号图片来介绍与单个规则相对应的每个代码片段。在实践中,通常允许规则由一些定义明确的符号(例如,编写编译器的语言)中的小段代码组成,以便简单的语义函数可以“内联”写出。在这种宽松的符号中,图 4.1中第一个产生式的规则可能只是E 1 .val : = E 2 .val + T.val。再举一个例子,假设我们想要计算逗号分隔列表的元素数量:

In a strict definition of attribute grammars, copy rules and semantic function calls are the only two kinds of permissible rules. In our examples we use a symbol to introduce each code fragment corresponding to a single rule. In practice, it is common to allow rules to consist of small fragments of code in some well-defined notation (e.g., the language in which a compiler is being written), so that simple semantic functions can be written out “in-line.” In this relaxed notation, the rule for the first production in Figure 4.1 might be simply E1.val : = E2.val + T.val. As another example, suppose we wanted to count the elements of a comma-separated list:

L → id LT图片LC := 1 + LT.c
LT →,L图片LT.c := Lc
LTε图片LT.c := 0

这里,第一个产生式的规则将左侧的c (计数)属性设置为比右侧尾部的计数多一。与显式语义函数一样,内联规则不允许引用当前产生式之外的任何变量或属性。我们将在第 4.4 节中介绍动作例程时放宽此限制。■

Here the rule on the first production sets the c (count) attribute of the left-hand side to one more than the count of the tail of the right-hand side. Like explicit semantic functions, in-line rules are not allowed to refer to any variables or attributes outside the current production. We will relax this restriction when we introduce action routines in Section 4.4. ■

语义函数的符号(无论是内联的还是显式的)和属性本身的类型都不是属性语法概念所固有的。语法的目的只是将含义与解析树或语法树的节点关联起来。为此,我们可以使用任何含义已经明确定义的符号和类型。在示例 4.44.5中,我们使用从普通算术中提取的语义函数将数值与 CFG 中的符号关联起来,从而与解析树节点关联起来。在完整编程语言的编译器或解释器中,树节点的属性可能包括

Neither the notation for semantic functions (whether in-line or explicit) nor the types of the attributes themselves is intrinsic to the notion of an attribute grammar. The purpose of the grammar is simply to associate meaning with the nodes of a parse tree or syntax tree. Toward that end, we can use any notation and types whose meanings are already well defined. In Examples 4.4 and 4.5, we associated numeric values with the symbols in a CFG—and thus with parse tree nodes—using semantic functions drawn from ordinary arithmetic. In a compiler or interpreter for a full programming language, the attributes of tree nodes might include

 对于标识符,引用符号表中有关它的信息

 for an identifier, a reference to information about it in the symbol table

 对于表达式,其类型

 for an expression, its type

 对于语句或表达式,引用编译器的中间形式中相应的代码

 for a statement or expression, a reference to corresponding code in the compiler's intermediate form

 对于几乎任何构造,都会指示相应源代码开始的文件名、行和列

 for almost any construct, an indication of the file name, line, and column where the corresponding source code begins

 对于任何内部节点,在下面的子树中发现的语义错误列表

 for any internal node, a list of semantic errors found in the subtree below

对于除翻译之外的其他目的(例如,在定理证明器或独立于机器的语言定义中),属性可能来自指称语义、操作语义公理语义的学科。感兴趣的读者可以在本章末尾的书目注释中找到参考资料。

For purposes other than translation—e.g., in a theorem prover or machine-independent language definition—attributes might be drawn from the disciplines of denotational, operational, or axiomatic semantics. Interested readers can find references in the Bibliographic Notes at the end of the chapter.

4.3 评估属性

4.3 Evaluating Attributes

例 4.6

Example 4.6

解析树的装饰

Decoration of a parse tree

评估属性的过程称为注释或解析树的修饰。图 4.2显示了如何使用图 4.1中的 AG修饰表达式(1 + 3) * 2的解析树。修饰完成后,可以在树根的 val 属性中找到整个表达式的值。■

The process of evaluating attributes is called annotation or decoration of the parse tree. Figure 4.2 shows how to decorate the parse tree for the expression (1 + 3) * 2, using the AG of Figure 4.1. Once decoration is complete, the value of the overall expression can be found in the val attribute of the root of the tree. ■

04-02-9780124104099
图 4.2 使用图 4.1中的属性文法对 (1 + 3) * 2 的解析树进行装饰。符号的 val 属性显示在框中。弯曲箭头表示属性流,在本例中严格向上。每个框包含单个语义规则的输出;进入框的箭头表示规则的输入。例如,在树的第二层,指向带有 8 的框的两个箭头表示应用规则 T 1 .val := product(T 2 .val, F.val)。

合成属性

Synthesized Attributes

图 4.1中的属性语法非常简单。每个符号最多有一个属性(标点符号没有)。此外,它们都是所谓的合成属性:它们的值仅在其符号出现在左侧的产生式中计算(合成)。对于像图 4.2中的带注释的解析树,这意味着属性流(信息从一个节点移动到另一个节点的模式)完全是自下而上的。

The attribute grammar of Figure 4.1 is very simple. Each symbol has at most one attribute (the punctuation marks have none). Moreover, they are all so-called synthesized attributes: their values are calculated (synthesized) only in productions in which their symbol appears on the left-hand side. For annotated parse trees like the one in Figure 4.2, this means that the attribute flow—the pattern in which information moves from node to node—is entirely bottom-up.

所有属性都经过合成的属性语法被称为S 属性语法。S 属性语法中语义函数的参数始终是当前产生式右侧符号的属性,返回值始终放在产生式左侧的属性中。标记(终端)通常具有内在属性(例如,标识符的字符串表示或数字常量的值);在编译器中,这些是扫描器初始化的合成属性。

An attribute grammar in which all attributes are synthesized is said to be S-attributed. The arguments to semantic functions in an S-attributed grammar are always attributes of symbols on the right-hand side of the current production, and the return value is always placed into an attribute of the left-hand side of the production. Tokens (terminals) often have intrinsic properties (e.g., the character-string representation of an identifier or the value of a numeric constant); in a compiler these are synthesized attributes initialized by the scanner.

继承的属性

Inherited Attributes

一般来说,我们可以想象(并且实际上也需要)这样的属性,当它们的符号位于当前产生式的右侧时,它们的值就会被计算出来。这样的属性被称为继承的。它们允许上下文信息从上方或侧面流入符号,这样,该产生式的规则就可以根据周围的上下文以不同的方式执行(或生成不同的值)。符号表信息通常通过继承的属性在符号之间传递。解析树根的继承属性也可用于表示外部环境(目标机器的特性、编译器的命令行参数等)。

In general, we can imagine (and will in fact have need of) attributes whose values are calculated when their symbol is on the right-hand side of the current production. Such attributes are said to be inherited. They allow contextual information to flow into a symbol from above or from the side, so that the rules of that production can be enforced in different ways (or generate different values) depending on surrounding context. Symbol table information is commonly passed from symbol to symbol by means of inherited attributes. Inherited attributes of the root of the parse tree can also be used to represent the external environment (characteristics of the target machine, command-line arguments to the compiler, etc.).

例 4.7

Example 4.7

自上而下的 CFG 和解析树进行减法

Top-down CFG and parse tree for subtraction

作为继承属性的一个简单示例,请考虑以下 LL(1) 表达式语法片段(这里仅涵盖减法):

As a simple example of inherited attributes, consider the following fragment of an LL(1) expression grammar (here covering only subtraction):

expr → const expr_tail

expr → const expr_tail

expr_tail → - const expr_tail | ε

expr_tail → -  const expr_tail | ε

对于表达式9 − 4 − 3,我们得到以下解析树:

For the expression 9 − 4 − 3, we obtain the following parse tree:

u04-01-9780124104099

如果我们想创建一个属性语法,将整个表达式的值累积到树的根中,我们会遇到一个问题:因为减法是左结合的,所以我们不能用一个数值来总结根的右子树。如果我们想用 S 属性语法自下而上地装饰树,我们必须准备在最顶层expr_tail节点的属性中描述任意数量的右操作数(参见练习 4.4)。这确实是可能的,但它违背了形式主义的目的:实际上,它要求我们将整个树嵌入到单个节点的属性中,并在单个语义函数中完成所有实际工作。

If we want to create an attribute grammar that accumulates the value of the overall expression into the root of the tree, we have a problem: because subtraction is left associative, we cannot summarize the right subtree of the root with a single numeric value. If we want to decorate the tree bottom-up, with an S-attributed grammar, we must be prepared to describe an arbitrary number of right operands in the attributes of the top-most expr_tail node (see Exercise 4.4). This is indeed possible, but it defeats the purpose of the formalism: in effect, it requires us to embed the entire tree into the attributes of a single node, and do all the real work inside a single semantic function.

例 4.8

Example 4.8

使用从左到右属性流进行装饰

Decoration with left-to-right attribute flow

但是,如果我们不仅可以自下而上传递属性值,还可以从左到右传递,那么我们可以将 9 传递最顶部的expr_tail节点,在那里它可以与 4 组合(以适当的左关联方式)。然后可以将得到的 5 传递到中间的expr_tail节点,与 3 组合成 2,然后向上传递到根节点:

If, however, we are allowed to pass attribute values not only bottom-up but also left-to-right in the tree, then we can pass the 9 into the top-most expr_tail node, where it can be combined (in proper left-associative fashion) with the 4. The resulting 5 can then be passed into the middle expr_tail node, combined with the 3 to make 2, and then passed upward to the root:

u04-02-9780124104099

例 4.9

Example 4.9

自上而下的 AG 减法

Top-down AG for subtraction

为了实现这种装饰风格,我们需要以下属性规则:

To effect this style of decoration, we need the following attribute rules:

expr → const expr_tail

expr → const expr_tail

 图片expr_tail.st := const.val

  expr_tail.st := const.val

 图片expr.val := expr_tail.val

  expr.val := expr_tail.val

expr_tail 1 → - const expr_tail 2

expr_tail1 → - const expr_tail2

 图片expr_tail 2.st := expr_tail 1.st − const.val

  expr_tail2.st := expr_tail1.st − const.val

 图片expr_tail 1.val := expr_tail 2.val

  expr_tail1.val := expr_tail2.val

expr_tailε

expr_tailε

 图片expr_tail.val := expr_tail.st

  expr_tail.val := expr_tail.st

在前两个产生式中,第一条规则用于将左侧上下文(迄今为止的表达式值)复制到“subtotal”(st)属性中;第二条规则将最终值从最右侧的叶子节点复制回根节点。在示例 4.8中的图片的expr_tail节点中,左侧框包含st属性;右侧框包含val。■

In each of the first two productions, the first rule serves to copy the left context (value of the expression so far) into a “subtotal” (st) attribute; the second rule copies the final value from the right-most leaf back up to the root. In the expr_tail nodes of the picture in Example 4.8, the left box holds the st attribute; the right holds val. ■

例 4.10

Example 4.10

自上而下的 AG 减法

Top-down AG for subtraction

我们可以充实示例 4.7的语法片段,以生成更完整的表达式语法,如图4.3所示(使用更短的符号名)。此语法的底层 CFG 接受与图 4.1中相同的语言,但前者是 SLR(l),而这个是 LL(1)。使用 LL(1) 语法对(1 + 3) * 2进行解析的属性流如图4.4所示。与示例 4.9的语法片段一样,每个运算符左操作数的值由 st(subtotal)属性带入TTFT生成式中。属性流的相对复杂性源于运算符是左结合的,但语法不能是左递归的:因此给定运算符的左操作数和右操作数出现在单独的生成式中。用于对实际语言执行语义分析的语法通常需要一些非 S 属性的流。 ■

We can flesh out the grammar fragment of Example 4.7 to produce a more complete expression grammar, as shown (with shorter symbol names) in Figure 4.3. The underlying CFG for this grammar accepts the same language as the one in Figure 4.1, but where that one was SLR(l), this one is LL(1). Attribute flow for a parse of (1 + 3) * 2, using the LL(1) grammar, appears in Figure 4.4. As in the grammar fragment of Example 4.9, the value of the left operand of each operator is carried into the TT and FT productions by the st (subtotal) attribute. The relative complexity of the attribute flow arises from the fact that operators are left associative, but the grammar cannot be left recursive: the left and right operands of a given operator are thus found in separate productions. Grammars to perform semantic analysis for practical languages generally require some non-S-attributed flow. ■

04-03-9780124104099
图 4.3 基于 LL(1) CFG 的常量表达式的属性语法。在此语法中,多个产生式具有两条语义规则。
04-04-9780124104099
图 4.4 使用图 4.3中的 AG 来装饰 (1 + 3) * 2 的自上而下的解析树。弯曲的箭头再次表示属性流;进入给定框的箭头表示单个语义规则的应用。在这种情况下,流不再严格自下而上,但仍是从左到右。在FTTT节点处,左侧框包含st属性;右侧框包含val

属性流

Attribute Flow

正如上下文无关语法没有指定应如何解析它一样,属性语法也没有指定应以何种顺序调用属性规则。换句话说,这两种表示法都是声明性的:它们定义了一组有效树,但没有说明如何构建或修饰它们。除此之外,这意味着对于给定的产生式,属性规则的列出顺序并不重要;属性流可能要求它们以任何顺序执行。如果在图 4.3中,我们要反转规则在产生式 1、2、3、5、6 和/或 7 中出现的顺序(首先列出 symbol.val 的规则),这将是一个纯粹表面上的改变;语法不会改变。

Just as a context-free grammar does not specify how it should be parsed, an attribute grammar does not specify the order in which attribute rules should be invoked. Put another way, both notations are declarative: they define a set of valid trees, but they don't say how to build or decorate them. Among other things, this means that the order in which attribute rules are listed for a given production is immaterial; attribute flow may require them to execute in any order. If, in Figure 4.3, we were to reverse the order in which the rules appear in productions 1, 2, 3, 5, 6, and/or 7 (listing the rule for symbol.val first), it would be a purely cosmetic change; the grammar would not be altered.

如果属性语法的规则为每棵可能的解析树的属性确定了一组唯一的值,我们就说该属性语法是定义良好的。如果属性语法永远不会导致解析树中的属性流图中存在循环,即如果任何解析树中的属性都永远不会(传递地)依赖于自身,则该属性语法是非循环的。(如果属性保证收敛到一个唯一的值,那么语法可以是循环的,并且仍然定义良好。)一般来说,实际的属性语法往往是非循环的。

We say an attribute grammar is well defined if its rules determine a unique set of values for the attributes of every possible parse tree. An attribute grammar is noncircular if it never leads to a parse tree in which there are cycles in the attribute flow graph—that is, if no attribute, in any parse tree, ever depends (transitively) on itself. (A grammar can be circular and still be well defined if attributes are guaranteed to converge to a unique value.) As a general rule, practical attribute grammars tend to be noncircular.

通过按照与树的属性流一致的顺序调用属性语法规则来修饰解析树的算法称为翻译方案。也许最简单的方案是反复遍历树,调用任何参数都已定义的语义函数,并在完成一次遍历且其中没有值发生变化时停止。这种方案被称为无知的,因为它不利用解析树或语法的特殊知识。只有语法定义明确时它才会停止。通过动态方案可以根据给解析树的结构调整求值顺序,可以实现更好的性能,至少对于非循环语法而言是这样——例如,通过构建属性流图的拓扑排序,然后按照与排序一致的顺序调用规则。

An algorithm that decorates parse trees by invoking the rules of an attribute grammar in an order consistent with the tree's attribute flow is called a translation scheme. Perhaps the simplest scheme is one that makes repeated passes over a tree, invoking any semantic function whose arguments have all been defined, and stopping when it completes a pass in which no values change. Such a scheme is said to be oblivious, in the sense that it exploits no special knowledge of either the parse tree or the grammar. It will halt only if the grammar is well defined. Better performance, at least for noncircular grammars, may be achieved by a dynamic scheme that tailors the evaluation order to the structure of a given parse tree—for example, by constructing a topological sort of the attribute flow graph and then invoking rules in an order consistent with the sort.

然而,最快的翻译方案往往是静态的——基于对属性语法本身结构的分析,然后机械地应用于由语法产生的任何树。与 LL 和 LR 解析器一样,线性时间静态翻译方案只能为某些受限的语法类别设计。S 属性语法(如图 4.1所示)是此类中最简单的一种。由于 S 属性语法中的属性流严格自下而上,因此可以通过访问解析树的节点来评估属性,访问顺序与 LR 系列解析器生成这些节点的顺序完全相同。事实上,可以在自下而上的解析过程中动态评估属性,从而交错解析和语义分析(属性评估)。

The fastest translation schemes, however, tend to be static—based on an analysis of the structure of the attribute grammar itself, and then applied mechanically to any tree arising from the grammar. Like LL and LR parsers, linear-time static translation schemes can be devised only for certain restricted classes of grammars. S-attributed grammars, such as the one in Figure 4.1, form the simplest such class. Because attribute flow in an S-attributed grammar is strictly bottom-up, attributes can be evaluated by visiting the nodes of the parse tree in exactly the same order that those nodes are generated by an LR-family parser. In fact, the attributes can be evaluated on the fly during a bottom-up parse, thereby interleaving parsing and semantic analysis (attribute evaluation).

图 4.3的属性语法比图 4.1的属性语法稍微混乱一些,但是它仍然是L 属性的:可以通过从左到右、深度优先遍历访问解析树的节点来评估其属性(与自上而下的解析过程中访问它们的顺序相同 - 参见图 4.4)。如果我们说属性As 依赖于属性Bt (如果Bt曾被传递给一个返回As值的语义函数),那么我们可以使用以下两个规则更正式地定义 L 属性语法:(1)左侧符号的每个合成属性仅依赖于该符号自己的继承属性或产生式右侧符号的属性(合成或继承);(2)右侧符号的每个继承属性仅依赖于左侧符号的继承属性或右侧其左侧符号的属性(合成或继承)。

The attribute grammar of Figure 4.3 is a good bit messier than that of Figure 4.1, but it is still L-attributed: its attributes can be evaluated by visiting the nodes of the parse tree in a single left-to-right, depth-first traversal (the same order in which they are visited during a top-down parse—see Figure 4.4). If we say that an attribute A.s depends on an attribute B.t if B.t is ever passed to a semantic function that returns a value for A.s, then we can define L-attributed grammars more formally with the following two rules: (1) each synthesized attribute of a left-hand-side symbol depends only on that symbol's own inherited attributes or on attributes (synthesized or inherited) of the production's right-hand-side symbols, and (2) each inherited attribute of a right-hand-side symbol depends only on inherited attributes of the left-hand-side symbol or on attributes (synthesized or inherited) of symbols to its left in the right-hand side.

因为 L 属性语法允许使用右侧符号的属性初始化产生式左侧属性的规则,所以每个 S 属性语法也是 L 属性语法。反之则不然:S 属性语法不允许初始化右侧的属性,因此存在非 S 属性的 L 属性语法。

Because L-attributed grammars permit rules that initialize attributes of the left-hand side of a production using attributes of symbols on the right-hand side, every S-attributed grammar is also an L-attributed grammar. The reverse is not the case: S-attributed grammars do not permit the initialization of attributes on the right-hand side, so there are L-attributed grammars that are not S-attributed.

S 属性属性语法是属性语法中最通用的一类,可以在 LR 解析期间动态执行求值。L 属性语法是属性语法中最通用的一类,可以在 LL 解析期间动态执行求值。如果我们将语义分析(以及可能的中间代码生成)与解析交错,则自下而上的解析器通常必须与 S 属性翻译方案配对;自上而下的解析器必须与 L 属性翻译方案配对。(根据语法的结构,自下而上的解析器通常可以容纳一些非 S 属性的属性流;我们将在 C-4.5.1 节中考虑这种可能性。)如果我们选择将解析和语义分析分成单独的过程,那么构建解析树或语法树的代码仍必须使用 S 属性或 L 属性翻译方案(视情况而定),但语义分析器可以根据需要使用更强大的方案。某些任务最容易通过非 L 属性方案来完成,例如生成“短路”布尔表达式的代码(将在第 6.1.56.4.1节中讨论)。

S-attributed attribute grammars are the most general class of attribute grammars for which evaluation can be implemented on the fly during an LR parse. L-attributed grammars are the most general class for which evaluation can be implemented on the fly during an LL parse. If we interleave semantic analysis (and possibly intermediate code generation) with parsing, then a bottom-up parser must in general be paired with an S-attributed translation scheme; a top-down parser must be paired with an L-attributed translation scheme. (Depending on the structure of the grammar, it is often possible for a bottom-up parser to accommodate some non-S-attributed attribute flow; we consider this possibility in Section C-4.5.1.) If we choose to separate parsing and semantic analysis into separate passes, then the code that builds the parse tree or syntax tree must still use an S-attributed or L-attributed translation scheme (as appropriate), but the semantic analyzer can use a more powerful scheme if desired. There are certain tasks, such as the generation of code for “short-circuit” Boolean expressions (to be discussed in Sections 6.1.5 and 6.4.1), that are easiest to accomplish with a non-L-attributed scheme.

一次性编译器

One-Pass Compilers

将语义分析和代码生成与解析交错进行的编译器被称为一次性编译器。3目前尚不清楚将语义分析与解析交错进行的编译器是使编译器更简单还是更复杂;这主要取决于个人喜好。如果中间代码生成与解析交错进行,则根本不需要构建语法树(当然,除非语法树中间代码)。此外,通常可以动态地将中间代码写入输出文件,而不是将其累积在解析树根的属性中。由此节省的空间对于主存储器非常小的上一代计算机来说非常重要。另一方面,在单独遍历语法树时更容易执行语义分析,因为该树比解析树更好地反映了程序的语义结构,尤其是使用自上而下的解析器时,并且因为可以选择以解析器选择的顺序以外的顺序遍历树。

A compiler that interleaves semantic analysis and code generation with parsing is said to be a one-pass compiler.3 It is unclear whether interleaving semantic analysis with parsing makes a compiler simpler or more complex; it's mainly a matter of taste. If intermediate code generation is interleaved with parsing, one need not build a syntax tree at all (unless of course the syntax tree is the intermediate code). Moreover, it is often possible to write the intermediate code to an output file on the fly, rather than accumulating it in the attributes of the root of the parse tree. The resulting space savings were important for previous generations of computers, which had very small main memories. On the other hand, semantic analysis is easier to perform during a separate traversal of a syntax tree, because that tree reflects the program's semantic structure better than the parse tree does, especially with a top-down parser, and because one has the option of traversing the tree in an order other than that chosen by the parser.

构建语法树

Building a Syntax Tree

例 4.11

Example 4.11

自下而上和自上而下的 AG 构建语法树

Bottom-up and top-down AGs to build a syntax tree

如果我们选择不交替进行解析和语义分析,我们仍然需要向上下文无关语法添加属性规则,但它们仅用于创建语法树,而不是强制执行语义规则或生成代码。图 4.54.6分别包含自下而上和自上而下的属性语法,用于为常量表达式构建语法树。这些语法中的属性既不包含数值也不包含目标代码片段;而是指向语法树的节点。函数make_leaf返回一个指向新分配的语法树节点的指针,该节点包含一个常量的值。函数make_un_opmake_bin_op分别返回指向新分配的语法树节点的指针,该节点包含一元或二元运算符,以及指向所提供操作数的指针。图 4.74.8分别展示了使用图 4.54.6的语法对(1 + 3) * 2的解析树进行修饰的各个阶段。请注意,每种情况下的最终语法树都是相同的。

If we choose not to interleave parsing and semantic analysis, we still need to add attribute rules to the context-free grammar, but they serve only to create the syntax tree—not to enforce semantic rules or generate code. Figures 4.5 and 4.6 contain bottom-up and top-down attribute grammars, respectively, to build a syntax tree for constant expressions. The attributes in these grammars hold neither numeric values nor target code fragments; instead they point to nodes of the syntax tree. Function make_leaf returns a pointer to a newly allocated syntax tree node containing the value of a constant. Functions make_un_op and make_bin_op return pointers to newly allocated syntax tree nodes containing a unary or binary operator, respectively, and pointers to the supplied operand(s). Figures 4.7 and 4.8 show stages in the decoration of parse trees for (1 + 3) * 2, using the grammars of Figures 4.5 and 4.6, respectively. Note that the final syntax tree is the same in each case.

04-05-9780124104099
图 4.5自下而上(S 属性)属性语法构造语法树。使用符号+ / (就像在计算器上一样)表示符号的变化。
04-06-9780124104099
图 4.6 自上而下(L 属性)属性语法构造语法树。此处的 st 属性与 ptr 属性类似(与图 4.3中的 st 属性不同),是指向语法树节点的指针。
04-07-9780124104099
图 4.7 使用图 4.5的语法,通过自下而上修饰解析树,为 (1 + 3) * 2 构建语法树。该图从下往上阅读。在图 (a) 中,常量 1 和 3 的值已放置在新的语法树叶子中。指向这些叶子的指针向上传播到ET的属性中。在 (b) 中,指向这些叶子的指针成为新的内部 + 节点的子指针。在 (c) 中,指向此节点的指针向上传播到T的属性中,并为 2 创建新的叶子。最后在 (d) 中,来自TF的指针成为新的内部 × 节点的子指针,并且指向此节点的指针向上传播到E的属性中。
04-08-9780124104099
图 4.8 使用图 4.6的语法,通过装饰自上而下的解析树来构建语法树。在上面的图 (a) 中,常数 1 的值已被放置在新的语法树叶子中。指向该叶子的指针然后传播到TT的 st 属性。在 (b) 中,创建了第二个叶子来保存常数 3。指向这两个叶子的指针然后成为新的内部 + 节点的子指针,指向该节点的指针从最底层TT的 st 属性(其创建位置)一直向上传播到最顶层FT的 st 属性。在 (c) 中,为常数 2 创建了第三个叶子。指向该叶子和 + 节点的指针然后成为新的 × 节点的子节点,指向该节点的指针从较低FT的 st(其创建位置)一直传播到树的根。

设计与实现

Design & Implementation

4.2 前向引用

4.2 Forward references

3.3.3和 C-3.4.1 节中,我们指出许多语言的作用域规则要求在使用名称之前先声明名称,并提供特殊机制来引入递归定义所需的前向引用。虽然这些规则可能有助于促进创建清晰、可维护的代码,但同样重要的动机(至少从历史上看)是促进单遍编译器的构建。随着内存大小、处理速度和程序员对代码质量改进的期望的增加,多遍编译器已经变得无处不在,语言设计者可以自由地(例如,在 C++、Java 和 C# 的类声明中)放弃声明先于使用的要求。

In Sections 3.3.3 and C-3.4.1 we noted that the scope rules of many languages require names to be declared before they are used, and provide special mechanisms to introduce the forward references needed for recursive definitions. While these rules may help promote the creation of clear, maintainable code, an equally important motivation, at least historically, was to facilitate the construction of one-pass compilers. With increases in memory size, processing speed, and programmer expectations regarding the quality of code improvement, multipass compilers have become ubiquitous, and language designers have felt free (as, for example, in the class declarations of C++, Java, and C#) to abandon the requirement that declarations precede uses.

04-01-9780124104099检查你的理解

Check Your Understanding

1. 什么决定了语言规则是语法问题还是静态语义问题?

1. What determines whether a language rule is a matter of syntax or of static semantics?

2. 为什么某些程序错误虽然可以在运行时检测出来,却无法在编译时检测出来?

2. Why is it impossible to detect certain program errors at compile time, even though they can be detected at run time?

3. 什么是属性语法

3. What is an attribute grammar?

4. 什么是编程断言?它们的用途是什么?

4. What are programming assertions? What is their purpose?

5. 合成属性继承属性有什么区别?

5. What is the difference between synthesized and inherited attributes?

6. 给出两个通常通过继承属性传递的信息的例子。

6. Give two examples of information that is typically passed through inherited attributes.

7. 什么是属性流

7. What is attribute flow?

8. 什么是一次性编译器?

8. What is a one-pass compiler?

9.定语语法中 S 定语L 定语非循环定语是什么意思?这些语法类别的意义是什么?

9. What does it mean for an attribute grammar to be S-attributed? L-attributed? Noncircular? What is the significance of these grammar classes?

4.4 动作例程

4.4 Action Routines

正如有自动工具可以为给定的上下文无关语法构建解析器一样,也有自动工具可以为给定的属性语法构建语义分析器(属性评估器)。属性评估器生成器语法分析已用于基于语法的编辑器 [ RT88 ]、增量编译器 [ SDB84 ]、网页布局 [ MTAB13 ] 以及编程语言研究的各个方面。然而,大多数生产编译器使用临时的手写翻译方案,将解析与语法树的构建交织在一起,在某些情况下,还会交织语义分析或中间代码生成的其他方面。因为它们在解析每个产品时评估其属性,所以它们不需要构建完整的解析树。

Just as there are automatic tools that will construct a parser for a given context-free grammar, there are automatic tools that will construct a semantic analyzer (attribute evaluator) for a given attribute grammar. Attribute evaluator generators have been used in syntax-based editors [RT88], incremental compilers [SDB84], web-page layout [MTAB13], and various aspects of programming language research. Most production compilers, however, use an ad hoc, handwritten translation scheme, interleaving parsing with the construction of a syntax tree and, in some cases, other aspects of semantic analysis or intermediate code generation. Because they evaluate the attributes of each production as it is parsed, they do not need to build the full parse tree.

与解析交错的临时翻译方案采用一组动作例程的形式。动作例程是程序员(语法编写者)指示编译器在解析中的特定点执行的语义函数。大多数解析器生成器都允许程序员指定动作例程。在 LL 解析器生成器中,动作例程可以出现在右侧的任何位置。解析器预测产生式后,将立即调用右侧开头的例程。解析器匹配(产生式)左侧符号后,将立即调用嵌入右侧中间的例程。实现机制很简单:当它预测产生式时,解析器将右侧的所有内容推送到堆栈上,包括终端(要匹配)、非终端(用于驱动未来的预测)和指向动作例程的指针。当它在解析堆栈顶部找到指向动作例程的指针时,解析器只需调用它,并将(指向)相应属性的指针作为参数传递。

An ad hoc translation scheme that is interleaved with parsing takes the form of a set of action routines. An action routine is a semantic function that the programmer (grammar writer) instructs the compiler to execute at a particular point in the parse. Most parser generators allow the programmer to specify action routines. In an LL parser generator, an action routine can appear anywhere within a right-hand side. A routine at the beginning of a right-hand side will be called as soon as the parser predicts the production. A routine embedded in the middle of a right-hand side will be called as soon as the parser has matched (the yield of) the symbol to the left. The implementation mechanism is simple: when it predicts a production, the parser pushes all of the right-hand side onto the stack, including terminals (to be matched), nonterminals (to drive future predictions), and pointers to action routines. When it finds a pointer to an action routine at the top of the parse stack, the parser simply calls it, passing (pointers to) the appropriate attributes as arguments.

例 4.12

Example 4.12

自上而下的操作程序构建语法树

Top-down action routines to build a syntax tree

为了使这个过程更加具体,再次考虑我们的常量表达式的 LL(1) 语法。图 4.9显示了在解析此语法时构建语法树的动作例程。此语法与图 4.6中的语法之间的唯一区别是动作例程(此处用花括号分隔)嵌入在右侧的符号之中;所执行的工作是相同的。属性语法可以很容易地转换为具有动作例程的语法,这是因为属性语法是 L 属性的。如果它需要更复杂的流程,我们将无法将其转换为动作例程。■

To make this process more concrete, consider again our LL(1) grammar for constant expressions. Action routines to build a syntax tree while parsing this grammar appear in Figure 4.9. The only difference between this grammar and the one in Figure 4.6 is that the action routines (delimited here with curly braces) are embedded among the symbols of the right-hand sides; the work performed is the same. The ease with which the attribute grammar can be transformed into the grammar with action routines is due to the fact that the attribute grammar is L-attributed. If it required more complicated flow, we would not be able to cast it as action routines. ■

04-09-9780124104099
图 4.9使用动作例程构建语法树的 LL(I) 语法。

设计与实现

Design & Implementation

4.3 属性评估器

4.3 Attribute evaluators

基于形式属性语法的自动求值器在语言研究项目中很受欢迎,因为它们在语言定义发生变化时可以节省开发人员的时间。它们在基于语法的编辑器和增量编译器中很受欢迎,因为它们可以节省执行时间:当对程序进行小幅更改时,求值器可能能够“修补”树装饰,速度比从头开始重建它们快得多。然而,对于典型的编译器来说,基于形式属性语法的语义分析是过度的:它的开销比动作例程更高,而且实际上并没有为编译器编写者节省那么多工作。

Automatic evaluators based on formal attribute grammars are popular in language research projects because they save developer time when the language definition changes. They are popular in syntax-based editors and incremental compilers because they save execution time: when a small change is made to a program, the evaluator may be able to “patch up” tree decorations significantly faster than it could rebuild them from scratch. For the typical compiler, however, semantic analysis based on a formal attribute grammar is overkill: it has higher overhead than action routines, and doesn't really save the compiler writer that much work.

例 4.13

Example 4.13

递归下降和动作例程

Recursive descent and action routines

与普通解析一样,递归下降和带有动作例程的表驱动解析之间有很大的相似性。图 4.10显示了图 2.17中的term_tail例程,该例程经过修改以完成其在构建语法树中的部分工作。此例程的行为反映了图 4.9中的产生式 2 到 4 的行为。该例程接受指向属性语法的TT 1中包含的语法树片段的指针作为参数。然后,给定输入中即将出现的 + 或 − 符号,它 (1) 调用add_op来解析该符号(返回字符串表示形式);(2) 调用 term 来解析属性语法的T;(3) 调用make_bin_op来创建一个新的树节点;(4) 将该节点传递给term_tail,后者解析属性语法的TT 2;以及 (5) 返回结果。■

As in ordinary parsing, there is a strong analogy between recursive descent and table-driven parsing with action routines. Figure 4.10 shows the term_tail routine from Figure 2.17, modified to do its part in constructing a syntax tree. The behavior of this routine mirrors that of productions 2 through 4 in Figure 4.9. The routine accepts as a parameter a pointer to the syntax tree fragment contained in the attribute grammar's TT1. Then, given an upcoming + or − symbol on the input, it (1) calls add_op to parse that symbol (returning a character string representation); (2) calls term to parse the attribute grammar's T; (3) calls make_bin_op to create a new tree node; (4) passes that node to term_tail, which parses the attribute grammar's TT2; and (5) returns the result. ■

传真:04-10-9780124104099
图 4.10 带有嵌入式“动作例程”的递归下降解析。与图 2.17中的同名例程以及图 4.9中的产生式 2 到 4 进行比较。

自下而上的评估

Bottom-Up Evaluation

在 LR 解析器生成器中,通常不能在右侧的任意位置嵌入动作例程,因为解析器通常只有在看到全部或大部分输出后才知道自己处于什么产生式中。因此,LR 解析器生成器只允许在右侧的部分(后缀)中嵌入动作例程被解析的产生式可以无歧义地标识出来(这称为尾部有歧义的前缀是左上角)。如果动作例程的属性流严格地自下而上(就像在 S 属性属性语法中一样),那么只需在右侧末尾执行即可。事实上,图 4.14.5的属性语法与动作例程版本基本相同。但是,如果动作例程负责很大一部分语义分析(而不是简单地构建语法树),那么它们通常需要上下文信息才能完成工作。为了在 LR 解析中获取和使用这些信息,它们需要一些(必然是有限的)对继承属性或当前产生式之外的信息的访问。我们将在 C-4.5.1 节中进一步讨论这个问题。

In an LR parser generator, one cannot in general embed action routines at arbitrary places in a right-hand side, since the parser does not in general know what production it is in until it has seen all or most of the yield. LR parser generators therefore permit action routines only in the portion (suffix) of the right-hand side in which the production being parsed can be identified unambiguously (this is known as the trailing part; the ambiguous prefix is the left corner). If the attribute flow of the action routines is strictly bottom-up (as it is in an S-attributed attribute grammar), then execution at the end of right-hand sides is all that is needed. The attribute grammars of Figures 4.1 and 4.5, in fact, are essentially identical to the action routine versions. If the action routines are responsible for a significant part of semantic analysis, however (as opposed to simply building a syntax tree), then they will often need contextual information in order to do their job. To obtain and use this information in an LR parse, they will need some (necessarily limited) access to inherited attributes or to information outside the current production. We consider this issue further in Section C-4.5.1.

4.5 属性的空间管理

4.5 Space Management for Attributes

任何属性评估方法都需要空间来保存语法符号的属性。如果我们要构建显式解析树,那么显而易见的方法是将属性存储在树本身的节点中。如果我们不构建解析树,那么我们需要找到一种方法来跟踪我们已经看到(或预测)但尚未完成解析的符号的属性。自下而上和自上而下的解析器的细节有所不同。

Any attribute evaluation method requires space to hold the attributes of the grammar symbols. If we are building an explicit parse tree, then the obvious approach is to store attributes in the nodes of the tree themselves. If we are not building a parse tree, then we need to find a way to keep track of the attributes for the symbols we have seen (or predicted) but not yet finished parsing. The details differ in bottom-up and top-down parsers.

对于具有 S 属性语法的自下而上的解析器,显而易见的方法是维护一个直接镜像解析堆栈的属性堆栈:解析堆栈上每个状态号旁边都有一个属性记录,记录了我们进入该状态时所移动的符号。解析器驱动程序会自动推送和弹出属性堆栈中的条目;空间管理对操作例程的编写者来说不是问题。如果我们尝试实现继承属性的效果,就会出现复杂情况,但这些可以在基本属性堆栈框架内解决。

For a bottom-up parser with an S-attributed grammar, the obvious approach is to maintain an attribute stack that directly mirrors the parse stack: next to every state number on the parse stack is an attribute record for the symbol we shifted when we entered that state. Entries in the attribute stack are pushed and popped automatically by the parser driver; space management is not an issue for the writer of action routines. Complications arise if we try to achieve the effect of inherited attributes, but these can be accommodated within the basic attribute-stack framework.

对于具有 L 属性语法的自上而下的解析器,我们有两个主要选项。第一个选项是自动的,但比自下而上的语法更复杂。它仍然使用属性堆栈,但不镜像解析堆栈。第二个选项具有较低的空间开销,并通过“缩短”复制规则来节省时间,但需要操作例程明确分配和释放属性的空间。

For a top-down parser with an L-attributed grammar, we have two principal options. The first option is automatic, but more complex than for bottom-up grammars. It still uses an attribute stack, but one that does not mirror the parse stack. The second option has lower space overhead, and saves time by “short-cutting” copy rules, but requires action routines to allocate and deallocate space for attributes explicitly.

在这两种解析器系列中,通常将一些操作例程的上下文信息保存在全局变量中。特别是符号表通常是全局的。我们传递当前活动范围的指示,而不是通过属性将其全部内容从一个产品传递到下一个产品。全局表中的查找然后使用此范围信息来获取正确的引用环境。

In both families of parsers, it is common for some of the contextual information for action routines to be kept in global variables. The symbol table in particular is usually global. Rather than pass its full contents through attributes from one production to the next, we pass an indication of the currently active scope. Lookups in the global table then use this scope information to obtain the right referencing environment.

04-02-9780124104099 更深入地

IN MORE DEPTH

我们在配套网站上更详细地讨论了属性空间管理。使用自下而上和自上而下的算术表达式语法,我们说明了自下而上和自上而下的解析器的自动管理,以及自上而下的解析器的临时选项。

We consider attribute space management in more detail on the companion site. Using bottom-up and top-down grammars for arithmetic expressions, we illustrate automatic management for both bottom-up and top-down parsers, as well as the ad hoc option for top-down parsers.

4.6 树文法和语法树装饰

4.6 Tree Grammars and Syntax Tree Decoration

到目前为止,我们在讨论中仅使用属性语法来修饰解析树。正如我们在章节介绍中提到的,属性语法也可用于修饰语法树。如果我们的编译器仅使用动作例程来构建语法树,那么大部分语义分析和中间代码生成将使用语法树作为基础。

In our discussion so far we have used attribute grammars solely to decorate parse trees. As we mentioned in the chapter introduction, attribute grammars can also be used to decorate syntax trees. If our compiler uses action routines simply to build a syntax tree, then the bulk of semantic analysis and intermediate code generation will use the syntax tree as base.

例 4.14

Example 4.14

具有类型的计算器语言的自下而上的 CFG

Bottom-up CFG for calculator language with types

图 4.11包含一个自下而上的 CFG,用于带有类型和声明的计算器语言。该语法与示例 2.37的语法有三点不同:(1) 我们允许声明与语句混合,(2) 我们区分整数和实数常量(假设后者包含小数点),(3) 我们要求整数和实数操作数之间进行显式转换。我们语言的预期语义要求每个标识符在使用前都进行声明,并且类型在计算中不能混合。■

Figure 4.11 contains a bottom-up CFG for a calculator language with types and declarations. The grammar differs from that of Example 2.37 in three ways: (1) we allow declarations to be intermixed with statements, (2) we differentiate between integer and real constants (presumably the latter contain a decimal point), and (3) we require explicit conversions between integer and real operands. The intended semantics of our language requires that every identifier be declared before it is used, and that types not be mixed in computations. ■

04-11-9780124104099
图 4.11 具有类型和声明的计算器语言的上下文无关语法。目的是每个标识符在使用前都应声明,并且类型在计算中不应混合。

例 4.15

Example 4.15

对整数和实数取平均值的语法树

Syntax tree to average an integer and a real

从图 4.5中的示例推断,很容易将语义函数或动作例程添加到图 4.11的语法中,以构建计算器语言的语法树(练习 4.21)。这种树的明显结构将表示我们在图 4.7中所做的表达式,并将程序表示为声明和语句的链接列表。作为一个具体的例子,图 4.12包含一个简单程序的语法树,用于打印整数和实数的平均值。■

Extrapolating from the example in Figure 4.5, it is easy to add semantic functions or action routines to the grammar of Figure 4.11 to construct a syntax tree for the calculator language (Exercise 4.21). The obvious structure for such a tree would represent expressions as we did in Figure 4.7, and would represent a program as a linked list of declarations and statements. As a concrete example, Figure 4.12 contains the syntax tree for a simple program to print the average of an integer and a real. ■

04-12-9780124104099
图 4.12 简单计算器程序的语法树。

例 4.16

Example 4.16

具有类型的计算器语言的树语法

Tree grammar for the calculator language with types

就像上下文无关语法描述了给定编程语言的解析树的可能结构一样,我们可以使用树语法来表示语法树的可能结构。与 CFG 一样,树语法的每个生成式都表示树中父级与其子级之间的可能关系。父级是生成式左侧的符号;子级是右侧的符号。图 4.12中使用的生成式可能如下所示:

Much as a context-free grammar describes the possible structure of parse trees for a given programming language, we can use a tree grammar to represent the possible structure of syntax trees. As in a CFG, each production of a tree grammar represents a possible relationship between a parent and its children in the tree. The parent is the symbol on the left-hand side of the production; the children are the symbols on the right-hand side. The productions used in Figure 4.12 might look something like the following:

程序项目

programitem

int_decl : itemid item

int_decl : itemid item

读取:itemid item

read : itemid item

real_decl : itemid item

real_decl : itemid item

写: itemexpr item

write : itemexpr item

空值:项目ε

null : itemε

‘÷’ : exprexpr expr

‘÷’ : exprexpr expr

‘+’ : exprexpr expr

‘+’ : exprexpr expr

浮点数:表达式表达式

float : exprexpr

id : exprε

id : exprε

实数常量 : exprε

real_const : exprε

这里,产生式左侧的符号A : B表示A是B的一个变体,并且可以出现在右侧预期出现B 的任何地方。■

Here the notation A : B on the left-hand side of a production means that A is one variant of B, and may appear anywhere a B is expected on a right-hand side. ■

树语法和上下文无关语法在重要方面有所不同。上下文无关语法旨在定义(生成)由标记字符串组成的语言,其中每个字符串都是解析树的边缘(产量)。解析是查找具有给定产量的树的过程。我们在这里使用的树语法旨在定义(或生成)树本身。我们不需要解析的概念:我们可以轻松检查树并确定它是否(以及如何)可以由语法生成。我们介绍树语法的目的是为语法树的装饰提供一个框架。附加到树语法的产生的语义规则可用于定义语法树的属性流,其方式与附加到上下文无关语法的产生的语义规则用于定义解析树的属性流完全相同。我们将在本节的其余部分使用树语法来执行静态语义检查。在第 15 章中,我们将展示如何使用额外的语义规则来生成中间代码。

Tree grammars and context-free grammars differ in important ways. A context-free grammar is meant to define (generate) a language composed of strings of tokens, where each string is the fringe (yield) of a parse tree. Parsing is the process of finding a tree that has a given yield. A tree grammar, as we use it here, is meant to define (or generate) the trees themselves. We have no need for a notion of parsing: we can easily inspect a tree and determine whether (and how) it can be generated by the grammar. Our purpose in introducing tree grammars is to provide a framework for the decoration of syntax trees. Semantic rules attached to the productions of a tree grammar can be used to define the attribute flow of a syntax tree in exactly the same way that semantic rules attached to the productions of a context-free grammar are used to define the attribute flow of a parse tree. We will use a tree grammar in the remainder of this section to perform static semantic checking. In Chapter 15 we will show how additional semantic rules can be used to generate intermediate code.

例 4.17

Example 4.17

Tree AG 是一种具有类型的计算器语言

Tree AG for the calculator language with types

使用图 4.13所示的节点类、变体和属性,可以构建具有类型的计算器语言的完整树属性语法。语法本身如图4.14所示。修饰后,语法树根处的程序节点将在一个合成属性中包含程序中所有静态语义错误的列表。(如果程序中没有此类错误,则列表为空。)每个项目expr节点都有一个继承属性 symtab,其中包含树左侧声明的所有标识符的列表(带有类型)。每个项目节点还有一个继承属性errors_in,列出在树左侧发现的所有静态语义错误,以及一个合成属性errors_out,用于将最终错误列表传播回根。每个expr节点都有一个合成属性指示其类型,另一个合成属性包含在其中发现的任何静态语义错误的列表。

A complete tree attribute grammar for our calculator language with types can be constructed using the node classes, variants, and attributes shown in Figure 4.13. The grammar itself appears in Figure 4.14. Once decorated, the program node at the root of the syntax tree will contain a list, in a synthesized attribute, of all static semantic errors in the program. (The list will be empty if the program is free of such errors.) Each item or expr node has an inherited attribute symtab that contains a list, with types, of all identifiers declared to the left in the tree. Each item node also has an inherited attribute errors_in that lists all static semantic errors found to its left in the tree, and a synthesized attribute errors_out to propagate the final error list back to the root. Each expr node has one synthesized attribute that indicates its type and another that contains a list of any static semantic errors found inside.

04-13-9780124104099
图 4.13 图 4.14的语法树属性文法的节点类。除名称外,给定类的所有变体都具有该类的所有属性。
04-14-978012410409904-15-978012410409904-16-9780124104099
图 4.14 属性语法,用于为具有类型的计算器语言装饰抽象语法树。我们使用方括号来分隔错误消息,使用尖括号来分隔符号表条目。并置表示错误消息中的连接;“+”和“-”运算符表示列表中的插入和删除。我们假设每个节点都已由扫描器或解析器中的动作例程初始化,以包含相应构造在源中出现的位置(行和列)的指示(参见练习 4.22)。“?”符号用作“通配符”;它匹配任何类型。

我们对语义错误的处理说明了一种常用技术。为了继续查找其他错误,我们必须为在没有错误的情况下设置的任何属性提供值。为了避免级联错误消息,我们为那些将在后续检查中悄悄通过的属性选择值。在这个特定情况下,我们使用一个名为error的伪类型,它我们将其与已经生成消息的任何符号表条目或表达式关联起来。

Our handling of semantic errors illustrates a common technique. In order to continue looking for other errors, we must provide values for any attributes that would have been set in the absence of an error. To avoid cascading error messages, we choose values for those attributes that will pass quietly through subsequent checks. In this specific case we employ a pseudotype called error, which we associate with any symbol table entry or expression for which we have already generated a message.

尽管需要进行一些检查才能验证这一事实,但我们的属性语法是非循环的并且定义明确。任何属性都不会被多次赋值。(图 4.14末尾的辅助例程应被视为宏,而不是语义函数。为了简洁起见,我们将整个树节点作为参数传递给它们。每个宏计算两个不同属性的值。在属性语法的严格表述下,每个宏将被两个单独的语义函数取代,每个计算属性一个。)■

Though it takes a bit of checking to verify the fact, our attribute grammar is noncircular and well defined. No attribute is ever assigned a value more than once. (The helper routines at the end of Figure 4.14 should be thought of as macros, rather than semantic functions. For the sake of brevity we have passed them entire tree nodes as arguments. Each macro calculates the values of two different attributes. Under a strict formulation of attribute grammars each macro would be replaced by two separate semantic functions, one per calculated attribute.) ■

例 4.18

Example 4.18

使用示例 4.17中的 AG 装饰一棵树

Decorating a tree with the AG of Example 4.17

图 4.15使用图 4.14中的语法来修饰图 4.12中的语法树。属性流的模式看起来比本章前面的示例要混乱得多,但这仅仅是因为类型检查比计算常量或构建语法树更复杂。符号表信息沿着项链流动并向下进入expr树。int_decl和real_decl节点添加新信息;其他节点只是传递表。类型信息在id: expr叶子处合成,方法是在符号表中查找标识符的名称。然后,该信息在表达式树中向上传播,并用于对运算符和赋值进行类型检查(后者未出现在此示例中)。错误消息通过 errorsjn 属性沿着项链流动,然后通过errors_out属性返回到根。消息也从expr树中向上流出。无论在何处执行类型检查,type 属性都可用于帮助创建新消息以附加到不断增长的消息列表中。■

Figure 4.15 uses the grammar of Figure 4.14 to decorate the syntax tree of Figure 4.12. The pattern of attribute flow appears considerably messier than in previous examples in this chapter, but this is simply because type checking is more complicated than calculating constants or building a syntax tree. Symbol table information flows along the chain of items and down into expr trees. The int_decl and real_decl nodes add new information; other nodes simply pass the table along. Type information is synthesized at id: expr leaves by looking up an identifier's name in the symbol table. The information then propagates upward within an expression tree, and is used to type-check operators and assignments (the latter don't appear in this example). Error messages flow along the chain of items via the errorsjn attributes, and then back to the root via the errors_out attributes. Messages also flow up out of expr trees. Wherever a type check is performed, the type attribute may be used to help create a new message to be appended to the growing message list. ■

04-17-9780124104099
图 4.15 使用图 4.14 中的语法装饰图 4.12 中语法我们假设位置信息已由解析器在每个节点中初始化;这些信息会导致错误消息,但不会在树中传播。

在我们的示例语法中,我们将错误消息累积到语法树根的合成属性中。在临时属性评估器中,我们可能会试图在发现错误时动态打印这些消息。然而,在实践中,特别是在多遍编译器中,缓冲消息是有意义的,这样它们就可以与编译器其他阶段生成的消息交错,并在编译结束时按程序顺序打印。

In our example grammar we accumulate error messages into a synthesized attribute of the root of the syntax tree. In an ad hoc attribute evaluator we might be tempted to print these messages on the fly as the errors are discovered. In practice, however, particularly in a multipass compiler, it makes sense to buffer the messages, so they can be interleaved with messages produced by other phases of the compiler, and printed in program order at the end of compilation.

可以使用自动属性求值器生成器将我们的属性语法转换为可执行代码。或者,可以以相互递归子例程的形式创建临时求值器(练习 4.20)。在后一种情况下,属性流将在例程的调用序列中明确显示。然后,我们可以根据需要选择将符号表保存在全局变量中,而不是通过属性将其从一个节点传递到另一个节点。大多数编译器都采用临时方法。

One could convert our attribute grammar into executable code using an automatic attribute evaluator generator. Alternatively, one could create an ad hoc evaluator in the form of mutually recursive subroutines (Exercise 4.20). In the latter case attribute flow would be explicit in the calling sequence of the routines. We could then choose if desired to keep the symbol table in global variables, rather than passing it from node to node through attributes. Most compilers employ the ad hoc approach.

04-01-9780124104099检查你的理解

Check Your Understanding

10. 语义功能和动作例程之间有什么区别?

10. What is the difference between a semantic function and an action routine?

11. 为什么不能将动作例程放置在 LR CFG 中产生式右侧的任意位置?

11. Why can't action routines be placed at arbitrary locations within the right-hand side of productions in an LR CFG?

12. 使用行动程序可以轻松捕获哪些属性流模式?

12. What patterns of attribute flow can be captured easily with action routines?

13. 一些编译器在操作例程中执行所有语义检查和中间代码生成。其他编译器使用操作例程构建语法树,然后在语法树的单独遍历中执行语义检查和中间代码生成。讨论这两种策略之间的权衡。

13. Some compilers perform all semantic checks and intermediate code generation in action routines. Others use action routines to build a syntax tree and then perform semantic checks and intermediate code generation in separate traversals of the syntax tree. Discuss the tradeoffs between these two strategies.

14. 动作例程通常将哪些类型的信息保存在全局变量中,而不是属性中?

14. What sort of information do action routines typically keep in global variables, rather than in attributes?

15. 描述上下文无关文法和树文法之间的相同点和不同点。

15. Describe the similarities and differences between context-free grammars and tree grammars.

16. 语义分析器如何避免产生级联错误消息?

16. How can a semantic analyzer avoid the generation of cascading error messages?

4.7 总结和结束语

4.7 Summary and Concluding Remarks

本章讨论了语义分析的任务。我们回顾了可分为语法、静态语义和动态语义的语言规则类型,并讨论了是否生成代码来执行动态语义检查的问题。我们还考虑了语义分析器在典型编译器中的作用。我们注意到,静态语义规则的执行和中间代码的生成都可以用解析树或语法树的注释或修饰来表示。然后,我们介绍了属性语法作为此修饰过程的正式框架。

This chapter has discussed the task of semantic analysis. We reviewed the sorts of language rules that can be classified as syntax, static semantics, and dynamic semantics, and discussed the issue of whether to generate code to perform dynamic semantic checks. We also considered the role that the semantic analyzer plays in a typical compiler. We noted that both the enforcement of static semantic rules and the generation of intermediate code can be cast in terms of annotation, or decoration, of a parse tree or syntax tree. We then presented attribute grammars as a formal framework for this decoration process.

属性语法将属性与上下文无关语法或树语法中的每个符号相关联,将属性规则与每个产生式相关联。在 CFG 中,仅在其符号出现在左侧的产生式中计算合成属性。标记的合成属性由扫描器初始化。继承属性在其符号出现在右侧的产生式中计算;它们允许符号下方子树中的计算依赖于符号出现的上下文。起始符号(目标)的继承属性可以表示编译器的外部环境。严格来说,属性语法只允许复制规则(将一个属性分配给另一个属性)和对语义函数的简单调用,但我们通常会放宽此限制以允许某些现有编程语言中或多或少的任意代码片段。

An attribute grammar associates attributes with each symbol in a context-free grammar or tree grammar, and attribute rules with each production. In a CFG, synthesized attributes are calculated only in productions in which their symbol appears on the left-hand side. The synthesized attributes of tokens are initialized by the scanner. Inherited attributes are calculated in productions in which their symbol appears within the right-hand side; they allow calculations in the subtree below a symbol to depend on the context in which the symbol appears. Inherited attributes of the start symbol (goal) can represent the external environment of the compiler. Strictly speaking, attribute grammars allow only copy rules (assignments of one attribute to another) and simple calls to semantic functions, but we usually relax this restriction to allow more or less arbitrary code fragments in some existing programming language.

正如可以根据使用上下文无关语法的解析算法对其进行分类一样,可以根据属性语法的属性流模式的复杂性对其进行分类。S 属性语法(其中所有属性都是合成的)可以自然地在对解析树的一次自下而上的传递中进行评估,其顺序与 LR 系列解析器发现树的顺序完全相同。L 属性语法(其中所有属性流都是从左到右的深度优先)可以按照 LL 系列解析器预测和匹配解析树的顺序进行评估。具有更复杂属性流模式的属性语法通常不用于生产编译器的解析树,但对于基于语法的编辑器、增量编译器和各种其他工具很有价值。

Just as context-free grammars can be categorized according to the parsing algorithm(s) that can use them, attribute grammars can be categorized according to the complexity of their pattern of attribute flow. S-attributed grammars, in which all attributes are synthesized, can naturally be evaluated in a single bottom-up pass over a parse tree, in precisely the order the tree is discovered by an LR-family parser. L-attributed grammars, in which all attribute flow is depth-first left-to-right, can be evaluated in precisely the order that the parse tree is predicted and matched by an LL-family parser. Attribute grammars with more complex patterns of attribute flow are not commonly used for the parse trees of production compilers, but are valuable for syntax-based editors, incremental compilers, and various other tools.

虽然可以构建自动工具来分析属性流和装饰解析树,但是大多数编译器都依赖于动作例程,编译器编写者将其嵌入到产生式的右侧,以评估解析中特定点的属性规则。在 LL 系列解析器中,动作例程可以嵌入到产生式右侧的任意点。在 LR 系列解析器中,动作例程必须遵循产生的左角自下而上的编译器中属性的空间自然与解析堆栈并行分配,但这使继承属性的管理变得复杂。自上而下的编译器中属性的空间可以自动分配,也可以由动作例程的编写者明确管理。自动方法具有规律性的优势,并且更易于维护;临时方法稍快一些,也更灵活。

While it is possible to construct automatic tools to analyze attribute flow and decorate parse trees, most compilers rely on action routines, which the compiler writer embeds in the right-hand sides of productions to evaluate attribute rules at specific points in a parse. In an LL-family parser, action routines can be embedded at arbitrary points in a production's right-hand side. In an LR-family parser, action routines must follow the production's left corner. Space for attributes in a bottom-up compiler is naturally allocated in parallel with the parse stack, but this complicates the management of inherited attributes. Space for attributes in a top-down compiler can be allocated automatically, or managed explicitly by the writer of action routines. The automatic approach has the advantage of regularity, and is easier to maintain; the ad hoc approach is slightly faster and more flexible.

单遍编译器中,扫描、解析、语义分析和代码生成交错进行,只对输入进行一次遍历,语义函数或操作例程负责所有的语义分析和代码生成。更常见的是,操作例程只是构建一个语法树,然后在后续遍历的单独遍历中对其进行修饰。这些遍历的代码通常是手写的,以相互递归的子例程的形式,允许编译器适应语法树上基本上任意的属性流。

In a one-pass compiler, which interleaves scanning, parsing, semantic analysis, and code generation in a single traversal of its input, semantic functions or action routines are responsible for all of semantic analysis and code generation. More commonly, action routines simply build a syntax tree, which is then decorated during separate traversal(s) in subsequent pass(es). The code for these traversals is usually written by hand, in the form of mutually recursive subroutines, allowing the compiler to accommodate essentially arbitrary attribute flow on the syntax tree.

在后续章节(特别是第 6-10 章)中,我们将考虑各种编程语言构造。我们不会介绍实现这些构造所需的实际属性语法,而是非正式地描述它们的语义,并给出目标代码的示例。我们将在第 15 章中返回属性语法,届时我们将更详细地考虑中间代码的生成。

In subsequent chapters (6–10 in particular) we will consider a wide variety of programming language constructs. Rather than present the actual attribute grammars required to implement these constructs, we will describe their semantics informally, and give examples of the target code. We will return to attribute grammars in Chapter 15, when we consider the generation of intermediate code in more detail.

4.8 练习

4.8 Exercises

4.1 自动机理论的基本结果告诉我们,语言L = a n b n c n = ε , abc , aabbcc , aaabbbccc , … 不是上下文无关的。但是,可以使用属性语法来捕获它。给出一个底层 CFG 和一组属性规则,这些规则将布尔属性ok与每个解析树的根R关联起来,使得当且仅当对应于树边缘的字符串在L中时, R.ok = true

4.1 Basic results from automata theory tell us that the language L = anbncn = ε, abc, aabbcc, aaabbbccc, … is not context free. It can be captured, however, using an attribute grammar. Give an underlying CFG and a set of attribute rules that associates a Boolean attribute ok with the root R of each parse tree, such that R.ok = true if and only if the string corresponding to the fringe of the tree is in L.

4.2修改 图 2.25中的文法,使其仅接受包含至少一个write语句的程序。对练习 2.17的解答做同样的修改。根据您的经验,您如何看待使用 CFG 来强制执行 C 中每个函数必须包含至少一个return语句的规则?

4.2 Modify the grammar of Figure 2.25 so that it accepts only programs that contain at least one write statement. Make the same change in the solution to Exercise 2.17. Based on your experience, what do you think of the idea of using the CFG to enforce the rule that every function in C must contain at least one return statement?

4.3给出两个 无法以合理成本进行检查的合理语义规则的例子,无论是静态检查还是由编译器在运行时生成的代码进行检查。

4.3 Give two examples of reasonable semantic rules that cannot be checked at reasonable cost, either statically or by compiler-generated code at run time.

4.4根据 示例 4.7中的 CFG 编写一个 S 属性属性语法,将整个表达式的值累积到树的根中。您需要使用动态内存分配,以便各个属性可以容纳任意数量的信息。

4.4 Write an S-attributed attribute grammar, based on the CFG of Example 4.7, that accumulates the value of the overall expression into the root of the tree. You will need to use dynamic memory allocation so that individual attributes can hold an arbitrary amount of information.

4.5  Lisp 具有一个不寻常的特性,即其程序采用带括号的列表的形式。因此,Lisp 程序的自然语法树是一棵二进制单元树(在 Lisp 中称为 cons 单元),其中第一个子节点表示列表的第一个元素,第二个子节点表示列表的其余部分。(cdr '(abc))的语法树如图4.16所示。(符号 ' L(quote L)的语法糖。)扩展练习 2.18

的 CFG以创建将构建此类树的属性语法。当一棵解析树完全修饰后,根应该有一个引用语法树的属性v。你可以假设每个原子都有一个合成属性v,它引用一个保存来自扫描器的信息的语法树节点。在你的语义函数中,你可以假设有一个cons函数,它以两个引用作为参数,并返回一个包含这些引用的新 cons 单元的引用。

4.5 Lisp has the unusual property that its programs take the form of parenthesized lists. The natural syntax tree for a Lisp program is thus a tree of binary cells (known in Lisp as cons cells), where the first child represents the first element of the list and the second child represents the rest of the list. The syntax tree for (cdr '(a b c)) appears in Figure 4.16. (The notation 'L is syntactic sugar for (quote L).)

Extend the CFG of Exercise 2.18 to create an attribute grammar that will build such trees. When a parse tree has been fully decorated, the root should have an attribute v that refers to the syntax tree. You may assume that each atom has a synthesized attribute v that refers to a syntax tree node that holds information from the scanner. In your semantic functions, you may assume the availability of a cons function that takes two references as arguments and returns a reference to a new cons cell containing those references.

04-18-9780124104099
图 4.16 Lisp 表达式 (cdr '(abc)) 的自然语法树。

4.6 回顾练习 2.13中的上下文无关文法。向文法中添加属性规则,将程序字符串中括号嵌套的最大深度计数累积到树的根中。例如,给定字符串f1(a, f2(b * (c + (d − (e − f))))),树根处的语句应具有计数为 3 的属性(参数列表周围的括号不计算在内)。

4.6 Refer back to the context-free grammar of Exercise 2.13. Add attribute rules to the grammar to accumulate into the root of the tree a count of the maximum depth to which parentheses are nested in the program string. For example, given the string f1(a, f2(b * (c + (d − (e − f))))),the stmt at the root of the tree should have an attribute with a count of 3 (the parentheses surrounding argument lists don't count).

4.7 假设我们要将常量表达式翻译成后缀表示法,即逻辑学家 Jan Lukasiewicz 的“逆波兰”表示法。后缀表示法不需要括号。它出现在基于堆栈的语言中,如 Postscript、Forth 以及1.4 节中提到的 P 码和 Java 字节码中间形式。在历史上,它也曾用作惠普生产的某些手持计算器的输入语言。当给定一个数字时,后缀计算器会将该数字压入内部堆栈。当给定一个运算符时,它会从堆栈中弹出前两个数字,应用运算符,然后压入结果。显示屏会在堆栈顶部显示该值。例如,要计算 2 × (15 − 3)/4,需要压入 2 04-03-97801241040991 5 04-03-97801241040993 04-03-9780124104099− * 4 04-03-9780124104099/ (这里04-03-9780124104099是“enter”键,用于结束构成数字的数字串)。使用图 4.1

的底层 CFG ,编写一个属性语法,将解析树的根与后缀计算器按钮按下序列seq关联起来,该序列将计算从该符号派生的标记的算术值。您可以假设存在一个函数buttons(c) ,它返回常数c的按钮按下序列(以后缀计算器结尾)。您还可以假设存在一个用于按钮按下序列的连接函数。04-03-9780124104099

4.7 Suppose that we want to translate constant expressions into the postfix, or “reverse Polish” notation of logician Jan Lukasiewicz. Postfix notation does not require parentheses. It appears in stack-based languages such as Postscript, Forth, and the P-code and Java bytecode intermediate forms mentioned in Section 1.4. It also served, historically, as the input language of certain hand-held calculators made by Hewlett-Packard. When given a number, a postfix calculator would push the number onto an internal stack. When given an operator, it would pop the top two numbers from the stack, apply the operator, and push the result. The display would show the value at the top of the stack. To compute 2 × (15 − 3)/4, for example, one would push 2 1 5 3 − * 4 / (here is the “enter” key, used to end the string of digits that constitute a number).

Using the underlying CFG of Figure 4.1, write an attribute grammar that will associate with the root of the parse tree a sequence of postfix calculator button pushes, seq, that will compute the arithmetic value of the tokens derived from that symbol. You may assume the existence of a function buttons(c) that returns a sequence of button pushes (ending with on a postfix calculator) for the constant c. You may also assume the existence of a concatenation function for sequences of button pushes.

4.8使用 图 4.3的底层 CFG 重复前面的练习。

4.8 Repeat the previous exercise using the underlying CFG of Figure 4.3.

4.9 考虑以下逆波兰算术表达式的文法:EEE op | id op → + | − | * | /假设每个 id 都有一个字符串类型的合成属性,并且每个Eop都有一个字符串类型的属性val,编写一个属性文法,安排解析树根的val属性包含将表达式翻译成传统的中缀表示法。例如,如果树的叶子从左到右为“AAB − * C /”,则根的 val 字段为“( (A*(A − B))/C ) ”。作为额外的挑战,编写一个属性文法版本,利用通常的算术优先级和结合性规则尽可能少地使用括号。

 

 

4.9 Consider the following grammar for reverse Polish arithmetic expressions:

 EE E op | id

 op → + | − | * | /

Assuming that each id has a synthesized attribute name of type string, and that each E and op has an attribute val of type string, write an attribute grammar that arranges for the val attribute of the root of the parse tree to contain a translation of the expression into conventional infix notation. For example, if the leaves of the tree, left to right, were “A A B − * C /,” then the val field of the root would be “( (A*(A − B))/C ).” As an extra challenge, write a version of your attribute grammar that exploits the usual arithmetic precedence and associativity rules to use as few parentheses as possible.

4.10 为了减少印刷错误的可能性,大多数信用卡号的数字都设计成满足所谓的Luhn 公式,该公式由 ANSI 在 20 世纪 60 年代标准化,以 IBM 数学家 Hans Peter Luhn 的名字命名。从右边开始,我们将每隔一位数字加倍(倒数第二位、倒数第四位等)。如果加倍后的数值为 10 或更大,我们将结果数字。然后我们将所有数字相加。对于任何有效数字,结果都是 10 的倍数。例如,1234 5678 9012 3456 变为 2264 1658 9022 6416,总和为 64,因此这不是有效数字。但是,如果最后一位数字是 2,总和将是 60,因此该数字可能是有效的。

为数字字符串提供一个属性语法,该属性语法会累积到解析树的根中,并根据 Luhn 公式指示该字符串是否有效。您的语法应该适应任意长度的字符串。

4.10 To reduce the likelihood of typographic errors, the digits comprising most credit card numbers are designed to satisfy the so-called Luhn formula, standardized by ANSI in the 1960s, and named for IBM mathematician Hans Peter Luhn. Starting at the right, we double every other digit (the second-to-last, fourth-to-last, etc.). If the doubled value is 10 or more, we add the resulting digits. We then sum together all the digits. In any valid number the result will be a multiple of 10. For example, 1234 5678 9012 3456 becomes 2264 1658 9022 6416, which sums to 64, so this is not a valid number. If the last digit had been 2, however, the sum would have been 60, so the number would potentially be valid.

Give an attribute grammar for strings of digits that accumulates into the root of the parse tree a Boolean value indicating whether the string is valid according to Luhn's formula. Your grammar should accommodate strings of arbitrary length.

4.11 考虑以下浮点常量的 CFG,不带指数表示法。(请注意,这个练习有些不自然:所讨论的语言是常规语言,可以由典型编译器的扫描器处理。)C数字.数字数字数字 更多数字更多数字数字| ε数字→ 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9用属性规则扩充此语法,这些规则会将常量的值累积到解析树根的 val 属性中。您的答案应该是 S 属性。

 

 

 

 

4.11 Consider the following CFG for floating-point constants, without exponential notation. (Note that this exercise is somewhat artificial: the language in question is regular, and would be handled by the scanner of a typical compiler.)

 Cdigits . digits

 digitsdigit more_digits

 more_digitsdigits | ε

 digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Augment this grammar with attribute rules that will accumulate the value of the constant into a val attribute of the root of the parse tree. Your answer should be S-attributed.

4.12 对上一个问题的明显解决方案的一个潜在批评是,解析树内部节点的值不反映上下文中其下方边缘的值。创建一个替代解决方案来解决这个批评。更具体地说,以这样的方式创建语法,即内部节点的值是其子节点的的总和。通过绘制12.34的解析树和属性流来说明您的解决方案。(提示:您可能需要不同的底层 CFG 和非 L 属性流。)

4.12 One potential criticism of the obvious solution to the previous problem is that the values in internal nodes of the parse tree do not reflect the value, in context, of the fringe below them. Create an alternative solution that addresses this criticism. More specifically, create your grammar in such a way that the val of an internal node is the sum of the vals of its children. Illustrate your solution by drawing the parse tree and attribute flow for 12.34. (Hint: You will probably want a different underlying CFG, and non-L-attributed flow.)

4.13 根据练习 2.11的 CFG,考虑以下变量声明的属性语法:decl → ID decl_tail decl.t := decl_tail.t decl_tail.in_tab := insert (decl.in_tab, ID.n, decl_tail.t) decl.out_tab := decl_tail.out_tab decl_tail →, decl decl_tail.t := decl.t decl.in_tab := decl_tail.in_tab decl_tail.out_tab := decl.out_tab decl_tail −→ : ID ; decl_tail.t := ID.n decl_tail.out_tab := decl_tail.in_tab

 

  图片

  图片

  图片

 

  图片

  图片

  图片

 

  图片

  图片

显示字符串A, B : C ; 的解析树。然后,使用箭头和文本描述,指定完全装饰树所需的属性流。(提示:请注意,语法不是L属性的。)

4.13 Consider the following attribute grammar for variable declarations, based on the CFG of Exercise 2.11:

 decl → ID decl_tail

   decl.t := decl_tail.t

   decl_tail.in_tab := insert (decl.in_tab, ID.n, decl_tail.t)

   decl.out_tab := decl_tail.out_tab

 decl_tail →, decl

   decl_tail.t := decl.t

   decl.in_tab := decl_tail.in_tab

   decl_tail.out_tab := decl.out_tab

 decl_tail −→ : ID ;

   decl_tail.t := ID.n

   decl_tail.out_tab := decl_tail.in_tab

Show a parse tree for the string A, B : C;. Then, using arrows and textual description, specify the attribute flow required to fully decorate the tree. (Hint: Note that the grammar is not L-attributed.)

4.14 能够处理非 L 属性属性流的基于 CFG 的属性评估器需要以解析树作为输入。解释如何在自上而下或自下而上的解析过程中自动构建解析树(即,无需显式操作例程)。

4.14 A CFG-based attribute evaluator capable of handling non-L-attributed attribute flow needs to take a parse tree as input. Explain how to build a parse tree automatically during a top-down or bottom-up parse (i.e., without explicit action routines).

4.15在 例 4.13 的基础上,修改图 2.17的递归下降解析器的其余部分,以构建计算器语言程序的语法树。

4.15 Building on Example 4.13, modify the remainder of the recursive descent parser of Figure 2.17 to build syntax trees for programs in the calculator language.

4.16编写一个具有动作例程和自动属性空间管理的 LL(1) 语法,以生成 练习 4.7中描述的逆波兰翻译。

4.16 Write an LL(1) grammar with action routines and automatic attribute space management that generates the reverse Polish translation described in Exercise 4.7.

4.17 

4.17 

(a)为 x中的多项式写一个上下文无关文法。添加语义函数来生成一个属性文法,该属性文法会将多项式的导数(作为字符串)累积在解析树根的合成属性中。

(a) Write a context-free grammar for polynomials in x. Add semantic functions to produce an attribute grammar that will accumulate the polynomial's derivative (as a string) in a synthesized attribute of the root of the parse tree.

(b)用 可以在解析期间评估的动作例程替换语义函数。

(b) Replace your semantic functions with action routines that can be evaluated during parsing.

4.18 

4.18 

(a) 用 Pascal 或 C 风格编写caseswitch语句的上下文无关文法。添加语义函数以确保相同的标签不会出现在构造的两个不同臂上。

(a) Write a context-free grammar for case or switch statements in the style of Pascal or C. Add semantic functions to ensure that the same label does not appear on two different arms of the construct.

(b)用 可以在解析期间评估的动作例程替换语义函数。

(b) Replace your semantic functions with action routines that can be evaluated during parsing.

4.19 编写一个算法来确定任意属性文法的规则是否为非循环的。(在最坏的情况下,你的算法将需要指数时间[ JOR75 ]。)

4.19 Write an algorithm to determine whether the rules of an arbitrary attribute grammar are noncircular. (Your algorithm will require exponential time in the worst case [JOR75].)

4.20用您喜欢的编程语言将 图 4.14中的属性文法重写为由相互递归子程序组成的临时树遍历形式。将符号表保存在全局变量中,而不是通过参数传递它。

4.20 Rewrite the attribute grammar of Figure 4.14 in the form of an ad hoc tree traversal consisting of mutually recursive subroutines in your favorite programming language. Keep the symbol table in a global variable, rather than passing it through arguments.

4.21 根据图 4.11的 CFG 编写一个属性语法,它将构建一棵具有图 4.14中所述结构的语法树。

4.21 Write an attribute grammar based on the CFG of Figure 4.11 that will build a syntax tree with the structure described in Figure 4.14.

4.22扩充 图 4.5图 4.6练习 4.21中的属性语法,在每个语法树节点中初始化一个合成属性,该属性指示相应结构在源程序中出现的位置(行和列)。您可以假设扫描器初始化每个标记的位置。

4.22 Augment the attribute grammar of Figure 4.5, Figure 4.6, or Exercise 4.21 to initialize a synthesized attribute in every syntax tree node that indicates the location (line and column) at which the corresponding construct appears in the source program. You may assume that the scanner initializes the location of every token.

4.23修改 图 4.114.14中的 CFG 和属性语法,以允许混合整数和实数表达式,而无需floattrunc。您将需要向必须强制转换为相反类型的任何节点添加注释,以便代码生成器知道生成代码来执行所以。一定要仔细考虑你的强制转换规则。例如,在表达式my_int + my_real中,你如何知道是否要将整数强制转换为实数,还是将实数强制转换为整数?

4.23 Modify the CFG and attribute grammar of Figures 4.11 and 4.14 to permit mixed integer and real expressions, without the need for float and trunc. You will want to add an annotation to any node that must be coerced to the opposite type, so that the code generator will know to generate code to do so. Be sure to think carefully about your coercion rules. In the expression my_int + my_real, for example, how will you know whether to coerce the integer to be a real, or to coerce the real to be an integer?

4.24解释 树形文法中产生式左侧需要AB符号的原因。为什么上下文无关文法不需要类似的符号?

4.24 Explain the need for the A ; B notation on the left-hand sides of productions in a tree grammar. Why isn't similar notation required for context-free grammars?

4.25 示例 4.17中的树属性语法的一个潜在缺点是,它反复将整个符号表从一个节点复制到另一个节点。在这个特定的微型语言中,很容易看出引用环境永远不会缩小:符号表只会随着新标识符的添加而改变。利用这一观察,说明如何修改图 4.14中的伪代码,使其仅复制指针,而不是整个符号表。

4.25 A potential objection to the tree attribute grammar of Example 4.17 is that it repeatedly copies the entire symbol table from one node to another. In this particular tiny language, it is easy to see that the referencing environment never shrinks: the symbol table changes only with the addition of new identifiers. Exploiting this observation, show how to modify the pseudocode of Figure 4.14 so that it copies only pointers, rather than the entire symbol table.

4.26 您对上一个练习的解决方案可能不适用于具有非平凡作用域规则的语言。解释如何修改图 4.14中的 AG以使用类似于第 C-3.4.1 节中描述的全局符号表。除其他事项外,您还应考虑嵌套作用域、在外部作用域中隐藏名称以及在使用变量之前声明变量的要求(第 C-3.4.1 节的表格未强制执行)。

4.26 Your solution to the previous exercise probably doesn't generalize to languages with nontrivial scoping rules. Explain how an AG such as that in Figure 4.14 might be modified to use a global symbol table similar to the one described in Section C-3.4.1. Among other things, you should consider nested scopes, the hiding of names in outer scopes, and the requirement (not enforced by the table of Section C-3.4.1) that variables be declared before they are used.

04-02-9780124104099 4.27–4.31  更深入。

4.27–4.31  In More Depth.

4.9 探索

4.9 Explorations

4.32 属性语法最具影响力的应用之一是 Cornell 合成器生成器 [ Rep84RT88 ]。了解生成器如何使用属性语法不仅对编辑中的程序中的语义信息进行增量更新,而且还根据形式语言规范自动创建基于语言的编辑器。这种技术有多通用?除了语法制导的计算机程序编辑之外,它还可能有哪些应用?

4.32 One of the most influential applications of attribute grammars was the Cornell Synthesizer Generator [Rep84, RT88]. Learn how the Generator used attribute grammars not only for incremental update of semantic information in a program under edit, but also for automatic creation of language based editors from formal language specifications. How general is this technique? What applications might it have beyond syntax-directed editing of computer programs?

4.33 本章中使用的属性语法都非常简单。大多数是 S 或 L 属性的。所有都是非循环的。更复杂的属性语法有什么实际用途吗?自动属性评估器怎么样?使用书目注释作为起点,对属性评估技术进行调查。实用技术和求知欲之间的界限在哪里?

4.33 The attribute grammars used in this chapter are all quite simple. Most are S- or L-attributed. All are noncircular. Are there any practical uses for more complex attribute grammars? How about automatic attribute evaluators? Using the Bibliographic Notes as a starting point, conduct a survey of attribute evaluation techniques. Where is the line between practical techniques and intellectual curiosities?

4.34 第一个经过验证的 Ada 实现是纽约大学 [ DGAFS + 80 ] 的 Ada/Ed 解释器。该解释器用基于集合的语言 SETL [ SDDS86 ] 编写,使用了 Ada 的指称语义定义。了解 Ada/Ed 项目、SETL 和指称语义。讨论使用形式定义如何帮助开发过程。还讨论了 Ada/Ed 的局限性,并扩展了形式语义在语言设计、开发和原型实现中的潜在作用。

4.34 The first validated Ada implementation was the Ada/Ed interpreter from New York University [DGAFS+80]. The interpreter was written in the set-based language SETL [SDDS86] using a denotational semantics definition of Ada. Learn about the Ada/Ed project, SETL, and denotational semantics. Discuss how the use of a formal definition aided the development process. Also discuss the limitations of Ada/Ed, and expand on the potential role of formal semantics in language design, development, and prototype implementation.

4.35  Scheme 语言手册 [ KCR + 98 ] 的第 5 版包含了 Scheme 在指称语义中的正式定义。与更传统的英语定义相比,这个定义有多长?它的可读性如何?长度和可读性水平说明了 Scheme 的什么问题?关于指称语义?(有关指称语义的更多信息,请参阅 Stoy [ Sto77 ] 或 Gordon [ Gor79 ] 的文章。)

手册的第 6 版 [ SDF + 07 ] 切换到操作语义。与指称版本相比如何?你认为标准委员会为什么做出这一改变?(有关更多信息,请参阅 Matthews 和 Findler 的论文 [ MF08 ]。)

4.35 Version 5 of the Scheme language manual [KCR+98] included a formal definition of Scheme in denotational semantics. How long is this definition, compared to the more conventional definition in English? How readable is it? What do the length and the level of readability say about Scheme? About denotational semantics? (For more on denotational semantics, see the texts of Stoy [Sto77] or Gordon [Gor79].)

Version 6 of the manual [SDF+07] switched to operational semantics. How does this compare to the denotational version? Why do you suppose the standards committee made the change? (For more information, see the paper by Matthews and Findler [MF08].)

04-02-9780124104099 4.36–4.37  更深入。

4.36–4.37  In More Depth.

4.10 书目注释

4.10 Bibliographic Notes

属性语法的早期理论大部分是由 Knuth [ Knu68 ] 开发的。Lewis、Rosenkrantz 和 Stearns [ LRS74 ] 引入了 L 属性语法的概念。Watt [ Wat77 ] 展示了如何在自下而上的解析器中使用标记符号模拟继承的属性。Jazayeri、Ogden 和 Rounds [ JOR75 ] 表明,在最坏的情况下,用任意属性流来装饰解析树可能需要指数级的时间。Courcelle [ Cou84 ] 和 Engelfriet [ Eng84 ] 的文章概述了属性评估的理论和实践。

Much of the early theory of attribute grammars was developed by Knuth [Knu68]. Lewis, Rosenkrantz, and Stearns [LRS74] introduced the notion of an L-attributed grammar. Watt [Wat77] showed how to use marker symbols to emulate inherited attributes in a bottom-up parser. Jazayeri, Ogden, and Rounds [JOR75] showed that exponential time may be required in the worst case to decorate a parse tree with arbitrary attribute flow. Articles by Courcelle [Cou84] and Engelfriet [Eng84] survey the theory and practice of attribute evaluation.

基于属性语法的语言程序编辑是由Reps 和 Teitelbaum 的Synthesizer Generator [ RT88 ](特定于语言的 Cornell 程序合成器 [ TR81 ]的后续产品)率先提出的。Magpie [ SDB84 ] 是一个早期的增量编译器,同样基于属性语法。Meyerovich 等人 [ MTAB13 ] 最近使用属性语法并行化了各种树遍历任务,尤其是用于网页渲染和 GPU 加速动画。

Language-based program editing based on attribute grammars was pioneered by the Synthesizer Generator [RT88] (a follow-on to the language-specific Cornell Program Synthesizer [TR81]) of Reps and Teitelbaum. Magpie [SDB84] was an early incremental compiler, again based on attribute grammars. Meyerovich et al. [MTAB13] have recently used attribute grammars to parallelize a variety of tree-traversal tasks—notably for web page rendering and GPU-accelerated animation.

在 Fischer 等人或 Appel [ App97 ]的文本中可以找到实现许多语言特性的动作例程。有关属性语法的更多注释,可以在 Cooper 和 Torczon [ CT04,第 171-188 页] 或 Aho 等人 [ ALSU07第 5 章]的文本中找到。

Action routines to implement many language features can be found in the texts of Fischer et al. or Appel [App97]. Further notes on attribute grammars can be found in the texts of Cooper and Torczon [CT04, pp. 171–188] or Aho et al. [ALSU07, Chap. 5].

Marcotty、Ledgard 和 Bochmann [ MLB76 ] 提供了编程语言语义形式化符号的早期概述。在 Winskel [ Win93 ] 和 Slonneger 和 Kurtz [ SK95 ] 的文本中可以找到更详细但仍有些过时的处理。Nipkow 和 Klein 从现代和数学严谨的角度介绍了该主题,将他们的文本与可执行定理证明系统 [ NK15 ] 相结合。

Marcotty, Ledgard, and Bochmann [MLB76] provide an early survey of formal notations for programming language semantics. More detailed but still somewhat dated treatment can be found in the texts of Winskel [Win93] and Slonneger and Kurtz [SK95]. Nipkow and Klein cover the topic from a modern and mathematically rigorous perspective, integrating their text with an executable theorem-proving system [NK15].

关于公理语义学的开创性论文由 Hoare [ Hoa69 ] 撰写。关于该主题的一本优秀书籍是 Gries 的《编程科学》 [ Gri81 ]。关于指称语义学的开创性论文由 Scott 和 Strachey [ SS71 ] 撰写。关于该主题的早期文本包括 Stoy [ Sto77 ] 和 Gordon [ Gor79 ]的文本。

The seminal paper on axiomatic semantics is by Hoare [Hoa69]. An excellent book on the subject is Gries's The Science of Programming [Gri81]. The seminal paper on denotational semantics is by Scott and Strachey [SS71]. Early texts on the subject include those of Stoy [Sto77] and Gordon [Gor79].


1除其他外,CAR Hoare(1934-)发明了快速排序算法和 case 语句,为 Algol W 的设计做出了贡献,并且是公理语义发展的领导者之一。在并发编程领域,他完善并形式化了监视器构造将在13.4.1 节中描述),并设计了 CSP 编程模型和符号。他于 1980 年获得 ACM 图灵奖。

1 Among other things, C. A. R. Hoare (1934–) invented the quicksort algorithm and the case statement, contributed to the design of Algol W, and was one of the leaders in the development of axiomatic semantics. In the area of concurrent programming, he refined and formalized the monitor construct (to be described in Section 13.4.1), and designed the CSP programming model and notation. He received the ACM Turing Award in 1980.

2语义规则的添加往往会使属性语法比上下文无关语法更加冗长。为了简洁起见,本章中的许多示例都使用了非常短的符号名称:使用E代替expr使用TT代替term_tail

2 The addition of semantic rules tends to make attribute grammars quite a bit more verbose than context-free grammars. For the sake of brevity, many of the examples in this chapter use very short symbol names: E instead of expr, TT instead of term_tail.

3大多数作者仅将“一次通过”一词用于一次性将源代码翻译成目标代码的编译器。有些作者坚持只在一次通过中生成中间代码,并允许通过额外的通过将中间代码翻译成目标代码。

3 Most authors use the term one-pass only for compilers that translate all the way from source to target code in a single pass. Some authors insist only that intermediate code be generated in a single pass, and permit additional pass(es) to translate intermediate code to target code.

5

目标机器架构

Target Machine Architecture

如第 1 章所述,编译器只是一个翻译器。它将用一种语言编写的程序翻译成用另一种语言编写的程序。第二种语言几乎可以是任何东西——其他一些高级语言、照相排版命令、VLSI(芯片)布局——但大多数时候它是某些可用计算机的机器语言。

As described in Chapter 1, a compiler is simply a translator. It translates programs written in one language into programs written in another language. This second language can be almost anything—some other high-level language, phototypesetting commands, VLSI (chip) layouts—but most of the time it's the machine language for some available computer.

正如存在许多不同的编程语言一样,机器语言也有许多不同的语言,尽管后者的多样性往往远低于前者。每种机器语言都对应一种不同的处理器体系结构。形式上,体系结构是硬件和软件之间的接口——软件是由编译器生成的语言,或由程序员为裸机编写的语言。处理器的实现是体系结构的具体实现,通常是硬件。要生成正确的代码,编译器编写者只要了解目标体系结构就足够了。要生成快速的代码,通常还需要了解实现,因为实现决定了给定语言结构的备选翻译的相对速度。

Just as there are many different programming languages, there are many different machine languages, though the latter tend to display considerably less diversity than the former. Each machine language corresponds to a different processor architecture. Formally, an architecture is the interface between the hardware and the software—that is, the language generated by a compiler, or by a programmer writing for the bare machine. The implementation of the processor is a concrete realization of the architecture, generally in hardware. To generate correct code, it suffices for a compiler writer to understand the target architecture. To generate fast code, it is generally necessary to understand the implementation as well, because it is the implementation that determines the relative speeds of alternative translations of a given language construct.

05-01-9780124104099 更深入地

IN MORE DEPTH

第 5 章的完整内容可以在配套网站上找到。它简要概述了处理器架构和实现方面,这些方面对编译器编写者来说特别重要,甚至可能值得以前看过该材料的读者复习一下。主要主题包括数据表示、指令集架构、实现技术的演变以及为现代处理器编译的挑战。示例主要来自 x86(一种主导台式机/笔记本电脑市场的传统 CISC(复杂指令集)架构)和 ARM(一种主导嵌入式、智能手机和平板电脑市场的更现代的 RISC(精简指令集)设计)。

Chapter 5 can be found in its entirety on the companion site. It provides a brief overview of those aspects of processor architecture and implementation of particular importance to compiler writers, and maybe worth reviewing even by readers who have seen the material before. Principal topics include data representation, instruction set architecture, the evolution of implementation techniques, and the challenges of compiling for modern processors. Examples are drawn largely from the x86, a legacy CISC (complex instruction set) architecture that dominates the desktop/laptop market, and the ARM, a more modern RISC (reduced instruction set) design that dominates the embedded, smart phone, and tablet markets.

设计与实现

Design & Implementation

5.1 伪汇编符号

5.1 Pseudo-assembly notation

在本书的其余部分中,我们需要考虑与某些高级语言结构相对应的机器指令序列。我们不会用某种特定处理器架构的汇编语言来表示这些序列,而是(在大多数情况下)依靠一种旨在表示通用 RISC 机器的简单符号。以下是一个简短的示例,它对 n元素浮点向量V的元素求和,并将结果放在s中:

At various times throughout the remainder of this book, we shall need to consider sequences of machine instructions corresponding to some high-level language construct. Rather than present these sequences in the assembly language of some particular processor architecture, we will (in most cases) rely on a simple notation designed to represent a generic RISC machine. The following is a brief example that sums the elements of an n-element floating-point vector, V, and places the results in s:

r1 = &V

r1 = &V

r2:= n

r2 := n

f1 := 0

f1 := 0

转到 L2

goto L2

L1:f2 := *r1 –– 加载

L1: f2 := *r1 –– load

f1 +:= f2

f1 +:= f2

r1 +:= 8 ––浮点数长度为 8 个字节

r1 +:= 8 –– floating-point numbers are 8 bytes long

r2 −:= 1

r2 −:= 1

L2:如果 r2 > 0 转到 L1

L2: if r2 > 0 goto L1

s := f1

s := f1

在大多数情况下,符号应该是不言自明的。它使用让人联想到高级语言的“赋值语句”和运算符,但每行代码对应一条机器指令,并且寄存器有明确命名(整数寄存器的名称以“ r ”开头;浮点寄存器的名称以“ f ”开头)。控制流完全基于 goto 和子程序调用(未显示)。条件测试假设硬件可以在一条指令中执行比较和分支,其中比较测试寄存器的内容与小常数或另一个寄存器的内容。

The notation should in most cases be self-explanatory. It uses “assignment statements” and operators reminiscent of high-level languages, but each line of code corresponds to a single machine instruction, and registers are named explicitly (the names of integer registers begin with 'r'; those of floating-point registers begin with 'f'). Control flow is based entirely on gotos and subroutine calls (not shown). Conditional tests assume that the hardware can perform a comparison and branch in a single instruction, where the comparison tests the contents of a register against a small constant or the contents of another register.

在我们的符号中,主存储器只能通过加载和存储指令来访问,这些指令看起来像是寄存器的赋值或赋值,没有算术运算。但是,我们确实假设可以使用位移寻址,这使我们能够访问位于寄存器中保存的地址的某个恒定偏移处的内存。例如,要将寄存器r1存储到位于距帧指针 ( fp ) 寄存器12 个字节偏移处的局部变量中,我们可以说*(fp−12) := r1

Main memory in our notation can be accessed only by load and store instructions, which look like assignments to or from a register, with no arithmetic. We do, however, assume the availability of displacement addressing, which allows us to access memory at some constant offset from the address held in a register. For example, to store register r1 to a local variable at an offset of 12 bytes from the frame pointer (fp) register, we could say *(fp−12) := r1.

语言设计的核心问题

Core Issues in Language Design

语言设计的核心问题

Core Issues in Language Design

在第一部分奠定了基础之后,我们现在来讨论大多数编程语言的核心问题:控制流、数据类型以及控制和数据的抽象。

Having laid the foundation in Part I, we now turn to issues that lie at the core of most programming languages: control flow, data types, and abstractions of both control and data.

第 6 章讨论控制流,包括表达式求值、排序、选择、迭代和递归。在许多情况下,我们将看到设计决策反映了有时互补但往往相互竞争的目标,即概念清晰和高效实现。几个问题,包括引用和值之间的区别以及应用(急切)求值和惰性求值之间的区别,将在后面的章节中再次出现。

Chapter 6 considers control flow, including expression evaluation, sequencing, selection, iteration, and recursion. In many cases we will see design decisions that reflect the sometimes complementary but often competing goals of conceptual clarity and efficient implementation. Several issues, including the distinction between references and values and between applicative (eager) and lazy evaluation, will recur in later chapters.

接下来的两章讨论了类型问题 第 7 章介绍了类型系统类型检查,包括等价性、兼容性和类型推断的概念。它还讨论了参数多态性问题,包括其隐式和显式(通用)形式。然后,第 8 章概述了具体的复合类型,包括记录和变体、数组、字符串、集合、指针、列表和文件。关于指针的部分介绍了垃圾收集技术。

The next two chapters consider the subject of types. Chapter 7 covers type systems and type checking, including the notions of equivalence, compatibility, and inference of types. It also considers the subject of parametric polymorphism, in both its implicit and explicit (generic) forms. Chapter 8 then presents a survey of concrete composite types, including records and variants, arrays, strings, sets, pointers, lists, and files. The section on pointers includes an introduction to garbage collection techniques.

控制和数据都易于抽象,即在简单且定义良好的接口背后隐藏复杂性的过程。控制抽象是第 9 章的主题。子程序是最常见的控制抽象,但我们也会考虑异常和协同程序,并简要回顾第 6 章中介绍的延续和迭代器主题。子程序的介绍侧重于调用序列和参数传递机制。

Both control and data are amenable to abstraction, the process whereby complexity is hidden behind a simple and well-defined interface. Control abstraction is the subject of Chapter 9. Subroutines are the most common control abstraction, but we also consider exceptions and coroutines, and return briefly to the subjects of continuations and iterators, introduced in Chapter 6. The coverage ofsubroutines focuses on calling sequences and on parameter-passing mechanisms.

第 10 章回归第 3 章中介绍的数据抽象主题。在许多现代语言中,该主题采用面向对象的形式,其特点是封装机制、继承和动态方法分派(子类型多态性)。我们对面向对象语言的介绍还将涉及构造函数、访问控制、泛型、闭包以及混合和多重继承。

Chapter 10 returns to the subject of data abstraction, introduced in Chapter 3. In many modern languages this subject takes the form of object orientation, characterized by an encapsulation mechanism, inheritance, and dynamic method dispatch (subtype polymorphism). Our coverage of object-oriented languages will also touch on constructors, access control, generics, closures, and mix-in and multiple inheritance.

6

控制流

Control Flow

讨论了编译器用来执行语义规则的机制(第 4 章)以及编译器必须为其生成代码的目标机器的特征(第 5 章)之后,我们现在回到语言设计的核心问题。具体来说,我们在本章中讨论程序执行中的控制流顺序问题。顺序是大多数计算模型的基础。它决定了应该先做什么,然后做什么,等等,以完成某项期望的任务。我们可以将用于指定顺序的语言机制分为几类:

Having considered the mechanisms that a compiler uses to enforce semantic rules (Chapter 4) and the characteristics of the target machines for which compilers must generate code (Chapter 5), we now return to core issues in language design. Specifically, we turn in this chapter to the issue of control flow or ordering in program execution. Ordering is fundamental to most models of computing. It determines what should be done first, what second, and so forth, to accomplish some desired task. We can organize the language mechanisms used to specify ordering into several categories:

1. 排序:语句要按照特定的顺序执行(或表达式求值),通常是它们在程序文本中出现的顺序。

1. Sequencing: Statements are to be executed (or expressions evaluated) in a certain specified order—usually the order in which they appear in the program text.

2. 选择:根据某些运行时条件,在两个或多个语句或表达式之间进行选择。最常见的选择结构是ifcase (switch)语句。选择有时也称为交替。

2. Selection: Depending on some run-time condition, a choice is to be made among two or more statements or expressions. The most common selection constructs are if and case (switch) statements. Selection is also sometimes referred to as alternation.

3. 迭代:一段给定的代码将被重复执行,要么执行一定次数,要么直到某个运行时条件为真。迭代结构包括for/do、whilerepeat循环。

3. Iteration: A given fragment of code is to be executed repeatedly, either a certain number of times, or until a certain run-time condition is true. Iteration constructs include for/do, while, and repeat loops.

4. 程序抽象:将一组可能复杂的控制结构(子程序)封装起来,使之可以被视为一个单元,通常可以进行参数化。

4. Procedural abstraction: A potentially complex collection of control constructs (a subroutine) is encapsulated in a way that allows it to be treated as a single unit, usually subject to parameterization.

5. 递归:表达式直接或间接地根据其自身(更简单的版本)进行定义;计算模型需要一个堆栈来保存有关表达式部分求值实例的信息。递归通常通过自引用子程序来定义。

5. Recursion: An expression is defined in terms of (simpler versions of) itself, either directly or indirectly; the computational model requires a stack on which to save information about partially evaluated instances of the expression. Recursion is usually defined by means of self-referential subroutines.

6. 并发性:两个或多个程序片段需要“同时”执行/评估,可以在单独的处理器上并行执行,也可以在单个处理器上交错执行,以实现相同的效果。

6. Concurrency: Two or more program fragments are to be executed/evaluated “at the same time,” either in parallel on separate processors, or interleaved on a single processor in a way that achieves the same effect.

7. 异常处理推测:程序片段以乐观的方式执行,假设某些预期条件将为真。如果该条件结果为假,执行分支到处理程序,该处理程序代替受保护片段的剩余部分执行(在异常处理的情况下),或代替整个保护片段执行(在推测的情况下)。对于推测,语言实现必须能够撤消或“回滚”受保护代码的任何可见效果。

7. Exception handling and speculation: A program fragment is executed optimistically, on the assumption that some expected condition will be true. If that condition turns out to be false, execution branches to a handler that executes in place of the remainder of the protected fragment (in the case of exception handling), or in place of the entire protected fragment (in the case of speculation). For speculation, the language implementation must be able to undo, or “roll back,” any visible effects of the protected code.

8. 不确定性:语句或表达式之间的顺序或选择故意不明确,这意味着任何替代方案都会产生正确的结果。有些语言要求选择是随机的,或者说是公平的,从某种正式意义上来说。

8. Nondeterminacy: The ordering or choice among statements or expressions is deliberately left unspecified, implying that any alternative will lead to correct results. Some languages require the choice to be random, or fair, in some formal sense of the word.

虽然语法和语义细节因语言而异,但这些类别涵盖了大多数编程语言中的所有控制流构造和机制。如果程序员能够按照这些类别而不是某种特定语言的语法来思考,那么他们就会发现学习新语言、评估语言之间的权衡以及以独立于语言的方式设计和推理算法会很容易。

Though the syntactic and semantic details vary from language to language, these categories cover all of the control-flow constructs and mechanisms found in most programming languages. A programmer who thinks in terms of these categories, rather than the syntax of some particular language, will find it easy to learn new languages, evaluate the tradeoffs among languages, and design and reason about algorithms in a language-independent way.

子程序是第 9 章的主题。并发是第 13 章的主题。这两章也讨论了异常处理和推测,分别在第 9.4 节13.4.4节。本章的大部分内容(第 6.3 节6.7节)用于讨论剩下的五个类别。我们从第 6.1 节开始考虑表达式的求值— 所有高级排序都基于此构建块。我们考虑表达式的句法形式、运算符的优先级和结合性、操作数的求值顺序以及赋值语句的语义。我们特别关注保存值的变量和保存对值的引用的变量之间的区别;这种区别在未来的章节中会多次发挥重要作用。在第 6.2 节中,我们考虑结构化非结构化(基于 goto)控制流之间的区别。

Subroutines are the subject of Chapter 9. Concurrency is the subject of Chapter 13. Exception handling and speculation are discussed in those chapters as well, in Sections 9.4 and 13.4.4. The bulk of the current chapter (Sections 6.3 through 6.7) is devoted to the five remaining categories. We begin in Section 6.1 by considering the evaluation of expressions—the building blocks on which all higher-level ordering is based. We consider the syntactic form of expressions, the precedence and associativity of operators, the order of evaluation of operands, and the semantics of the assignment statement. We focus in particular on the distinction between variables that hold a value and variables that hold a reference to a value; this distinction will play an important role many times in future chapters. In Section 6.2 we consider the difference between structured and unstructured (goto-based) control flow.

不同类别的控制流的相对重要性在不同类别的编程语言中存在很大差异。排序是命令式(冯·诺依曼和面向对象)语言的核心,但在函数式语言中却起着相对较小的作用,函数式语言强调表达式的求值,不强调或消除以除返回值以外的任何方式影响程序输出的语句(例如赋值)。同样,函数式语言大量使用递归,而命令式语言则倾向于强调迭代。逻辑语言倾向于完全不强调或隐藏控制流问题:程序员只需指定一组推理规则;语言实现必须找到应用这些规则的顺序,以便推导出满足某些所需属性的值。

The relative importance of different categories of control flow varies significantly among the different classes of programming languages. Sequencing is central to imperative (von Neumann and object-oriented) languages, but plays a relatively minor role in functional languages, which emphasize the evaluation of expressions, de-emphasizing or eliminating statements (e.g., assignments) that affect program output in any way other than through the return of a value. Similarly, functional languages make heavy use of recursion, while imperative languages tend to emphasize iteration. Logic languages tend to de-emphasize or hide the issue of control flow entirely: The programmer simply specifies a set of inference rules; the language implementation must find an order in which to apply those rules that will allow it to deduce values that satisfy some desired property.

6.1 表达式求值

6.1 Expression Evaluation

例 6.1

Example 6.1

典型的函数调用

A typical function call

表达式通常由一个简单对象(例如文字常量、命名变量或常量)或应用于集合的运算符或函数组成操作数或参数,每个操作数或参数又是一个表达式。通常使用术语运算符来表示使用特殊、简单语法的内置函数,使用术语操作数来表示运算符的参数。在大多数命令式语言中,函数调用由函数名称后跟括号中的逗号分隔的参数列表组成,例如

An expression generally consists of either a simple object (e.g., a literal constant, or a named variable or constant) or an operator or function applied to a collection of operands or arguments, each of which in turn is an expression. It is conventional to use the term operator for built-in functions that use special, simple syntax, and to use the term operand for an argument of an operator. In most imperative languages, function calls consist of a function name followed by a parenthesized, comma-separated list of arguments, as in

我的函数(A,B,C)

my_func(A, B, C)

例 6.2

Example 6.2

典型运算符

Typical operators

运算符通常更简单,仅采用一个或两个参数,并且不需要括号和逗号:

Operators are typically simpler, taking only one or two arguments, and dispensing with the parentheses and commas:

a + b

a + b

−c

− c

正如我们在3.5.2 节中看到的,有些语言将其运算符定义为更“正常”函数的语法糖。例如,在 Ada 中, a + b是“+”(a, b)的缩写;在 C++ 中,a + b是a.operator+(b)operator+(a, b)的缩写(以定义者为准)。■

As we saw in Section 3.5.2, some languages define their operators as syntactic sugar for more “normal”-looking functions. In Ada, for example, a + b is short for “+”(a, b); in C++, a + b is short for a.operator+(b) or operator+(a, b) (whichever is defined). ■

通常,一种语言可以指定函数调用(运算符调用)使用前缀、中缀或后缀表示法。这些术语分别表示函数名称出现在其几个参数之前、之中还是之后:

In general, a language may specify that function calls (operator invocations) employ prefix, infix, or postfix notation. These terms indicate, respectively, whether the function name appears before, among, or after its several arguments:

前缀:操作ab或者操作(a,b)或者上文ab)
中缀:一个op b
后缀ab操作

t0010

例 6.3

Example 6.3

剑桥波兰语(前缀)表示法

Cambridge Polish (prefix) notation

大多数命令式语言对二元运算符使用中缀表示法,对一元运算符和其他函数(用括号括住参数)使用前缀表示法。Lisp 对所有函数使用前缀表示法,但使用上述第三种变体:在所谓的剑桥波兰1表示法中,它将函数名称放在括号内:

Most imperative languages use infix notation for binary operators and prefix notation for unary operators and (with parentheses around the arguments) other functions. Lisp uses prefix notation for all functions, but with the third of the variants above: in what is known as Cambridge Polish1 notation, it places the function name inside the parentheses:

(* (+ 1 3) 2);中缀形式为 (1 + 3) * 2
(附加 abc my_list)

例 6.4

Example 6.4

机器学习中的并置

Juxtaposition in ML

ML 系列语言完全省去了括号,除非是为了消除歧义:

ML-family languages dispense with the parentheses altogether, except when they are required for disambiguation:

最大(2 + 3)4;;⇒5

max (2 + 3) 4;;      ⇒ 5

例 6.5

Example 6.5

Smalltalk 中的 Mixfìx 符号

Mixfìx notation in Smalltalk

一些语言(尤其是 ML 和 R 脚本语言)允许用户创建新的中缀运算符。 Smalltalk 对所有函数(称为消息)都使用中缀表示法,包括内置函数和用户定义函数。以下 Smalltalk 语句向图形对象myBox发送“ displayOn: at : ”消息,参数为myScreen100@50(像素位置)。它对应于其他语言调用的“ displayOn: at: ”函数,参数为myBox、myScreen100@50

A few languages, notably ML and the R scripting language, allow the user to create new infix operators. Smalltalk uses infix notation for all functions (which it calls messages), both built-in and user-defined. The following Smalltalk statement sends a “displayOn: at: “ message to graphical object myBox, with arguments myScreen and 100@50 (a pixel location). It corresponds to what other languages would call the invocation of the “displayOn: at:“ function with arguments myBox, myScreen, and 100@50.

myBox displayOn:我的屏幕位于:100@50

myBox displayOn: myScreen at: 100@50

例 6.6

Example 6.6

条件表达式

Conditional expressions

这种多词中缀表示法在其他语言中也偶尔出现。2Algol 中,我们可以说

This sort of multiword infix notation occurs occasionally in other languages as well.2 In Algol one can say

a :=如果 b <> 0 则 a/b 否则 0;

a := if b <> 0 then a/b else 0;

此处“ if… then … else ” 是三操作数中缀运算符。C 中的等效运算符写为“ … ? … : … ”:

Here “if… then … else“ is a three-operand infix operator. The equivalent operator in C is written “… ? … : …“:

a = b != 0 ?a/b : 0;

a = b != 0 ? a/b : 0;

Postscript、Forth(某些手持计算器的输入语言)和某些编译器的中间代码中的大多数函数都使用后缀表示法。后缀也出现在其他语言的几个地方。示例包括 Pascal 的指针解引用运算符 (S) 以及 C 及其后代的后置增量和减量运算符 (++ 和 −−)。

Postfix notation is used for most functions in Postscript, Forth, the input language of certain hand-held calculators, and the intermediate code of some compilers. Postfix appears in a few places in other languages as well. Examples include the pointer dereferencing operator(S) of Pascal and the post-increment and decrement operators (++ and −−) of C and its descendants.

6.1.1 优先级和结合性

6.1.1 Precedence and Associativity

例 6.7

Example 6.7

复杂的 Fortran 表达式

A complicated Fortran expression

大多数语言都提供了丰富的内置算术和逻辑运算符。当以中缀表示法编写时,没有括号,这些运算符会导致对什么是操作数的歧义。例如,在使用 ** 进行指数运算的 Fortran 中,我们应该如何解析a + b * c**d**e/f?这应该分组为

Most languages provide a rich set of built-in arithmetic and logical operators. When written in infix notation, without parentheses, these operators lead to ambiguity as to what is an operand of what. In Fortran, for example, which uses ** for exponentiation, how should we parse a + b * c**d**e/f? Should this be grouped as

((((a + b) * c)**d)**e)/f

((((a + b) * c)**d)**e)/f

或者

or

a + (((b * c)**d)**(e/f))

a + (((b * c)**d)**(e/f))

或者

or

a + ((b * (c**(d**e)))/f)

a + ((b * (c**(d**e)))/f)

或者还有其他选择?(在 Fortran 中,答案是显示的最后一个选项。)■

or yet some other option? (In Fortran, the answer is the last of the options shown.) ■

在任何给定的语言中,替代评估顺序的选择取决于运算符的优先级结合性,这些概念我们在第 2.1.3 节中介绍过。优先级和结合性问题不会出现在前缀或后缀表示法中。

In any given language, the choice among alternative evaluation orders depends on the precedence and associativity of operators, concepts we introduced in Section 2.1.3. Issues of precedence and associativity do not arise in prefix or postfix notation.

例 6.8

Example 6.8

四种有影响力的语言的优先地位

Precedence in four influential languages

优先级规则规定,某些运算符在没有括号的情况下会比其他运算符“更紧密地”分组。在大多数语言中,乘法和除法的分组比加法和减法更紧密,因此 2 + 3 × 4 等于 14 而不是 20。不过,不同语言之间的细节差别很大。图 6.1显示了几种著名语言的优先级别。■

Precedence rules specify that certain operators, in the absence of parentheses, group “more tightly” than other operators. In most languages multiplication and division group more tightly than addition and subtraction, so 2 + 3 × 4 is 14 and not 20. Details vary widely from one language to another, however. Figure 6.1 shows the levels of precedence for several well-known languages. ■

编号:F06-01-9780124104099
图 6.1 Fortran、Pascal、C 和 Ada 中的运算符优先级。图顶部的运算符组合最紧密

C 的优先级结构(以及其后代 C++、Java 和 C#,略有不同)比大多数其他语言的优先级结构丰富得多。事实上,它比图 6.1所示的更丰富,因为 C 语言中还有几个额外的构造,包括类型转换、函数调用、数组下标和记录字段选择,都被归类为运算符。可以公平地说,大多数 C 程序员并不记得他们语言的所有优先级。语言设计者的意图大概是确保当不使用括号强制执行特定的求值顺序时通常会发生“正确的事情”。然而,明智的程序员不会依赖这一点,而是查阅手册或添加括号。

The precedence structure of C (and, with minor variations, of its descendants, C++, Java, and C#) is substantially richer than that of most other languages. It is, in fact, richer than shown in Figure 6.1, because several additional constructs, including type casts, function calls, array subscripting, and record field selection, are classified as operators in C. It is probably fair to say that most C programmers do not remember all of their language's precedence levels. The intent of the language designers was presumably to ensure that “the right thing” will usually happen when parentheses are not used to force a particular evaluation order. Rather than count on this, however, the wise programmer will consult the manual or add parentheses.

例 6.9

Example 6.9

Pascal 优先级中的“陷阱”

A “gotcha” in Pascal precedence

也可以公平地说,Pascal 相对扁平的优先级层次结构是一个错误。新手 Pascal 程序员经常会写出这样的条件

It is also probably fair to say that the relatively flat precedence hierarchy of Pascal was a mistake. Novice Pascal programmers would frequently write conditions like

如果 A < B 且 C < D 那么 (* 哎哟 *)

if A < B and C < D then (* ouch *)

除非A、B、CD都是布尔类型(这种情况不太可能发生),否则此代码将导致静态语义错误,因为优先级规则会将其分组为A < (B 和 C) < D。(即使所有四个操作数都是布尔类型,结果也几乎肯定会与程序员的意图不同。)大多数语言通过赋予算术运算符比关系(比较)运算符更高的优先级来避免此问题,而关系(比较)运算符的优先级又高于逻辑运算符。值得注意的例外包括 APL 和 Smalltalk,其中所有运算符都具有相同的优先级;必须使用括号来指定分组。■

Unless A, B, C, and D were all of type Boolean, which is unlikely, this code would result in a static semantic error, since the rules of precedence cause it to group as A < (B and C) < D. (And even if all four operands were of type Boolean, the result was almost certain to be something other than what the programmer intended.) Most languages avoid this problem by giving arithmetic operators higher precedence than relational (comparison) operators, which in turn have higher precedence than the logical operators. Notable exceptions include APL and Smalltalk, in which all operators are of equal precedence; parentheses must be used to specify grouping. ■

例 6.10

Example 6.10

结合性的通用规则

Common rules for associativity

结合性规则指定了相同优先级的运算符序列是向右分组还是向左分组。这里的约定在不同的语言中稍微统一一些,但还是存在一些差异。基本算术运算符几乎总是从左到右结合,所以9 − 3 − 24而不是8。在 Fortran 中,如上所述,幂运算符 (**) 遵循标准数学约定,从右到左结合,所以4**3**2262144而不是4096。在 Ada 中,幂运算不结合:必须写成(4**3)**24**(3**2)语言语法不允许不带括号的形式。在允许在表达式内赋值的语言中(我们将在6.1.2 节中进一步讨论这一选项),赋值是从右到左关联的。因此在 C 中,a = b = a + c将a + c赋值给b,然后将相同的值赋值给a。■

Associativity rules specify whether sequences of operators of equal precedence group to the right or to the left. Conventions here are somewhat more uniform across languages, but still display some variety. The basic arithmetic operators almost always associate left-to-right, so 9 − 3 − 2 is 4 and not 8. In Fortran, as noted above, the exponentiation operator (**) follows standard mathematical convention, and associates right-to-left, so 4**3**2 is 262144 and not 4096. In Ada, exponentiation does not associate: one must write either (4**3)**2 or 4**(3**2); the language syntax does not allow the unparenthesized form. In languages that allow assignments inside expressions (an option we will consider more in Section 6.1.2), assignment associates right-to-left. Thus in C, a = b = a + c assigns a + c into b and then assigns the same value into a. ■

例 6.11

Example 6.11

Haskell 中的用户定义优先级和结合性

User-defined precedence and associativity in Haskell

Haskell 的独特之处在于它允许程序员指定用户定义运算符的结合性和优先级。例如,预定义的 ^ 运算符表示整数幂,在标准库中声明为(并且可以由程序员重新定义)

Haskell is unusual in allowing the programmer to specify both the associativity and the precedence of user-defined operators. The predefined ^ operator, for Example, which indicates integer exponentiation, is declared in the standard library (and could be redefined by the programmer) as

固定 8 ^

infixr 8 ^

此处 infixr 表示“右结合中缀运算符”,因此4 ^ 3 ^ 2分组为4 ^ (3 ^ 2)而不是(4 ^ 3) ^ 2。类似的 infixl 和 infix 声明分别指定左结合和非结合。优先级从 0(最松)到 9(最紧)。如果没有提供“固定性”声明,则新定义的运算符默认为左结合,并在 9 级分组。函数应用(在 Haskell 中通过并置简单指定)分组最紧 - 实际上是 10 级。■

Here infixr means “right associative infix operator,” so 4 ^ 3 ^ 2 groups as 4 ^ (3 ^ 2) rather than as (4 ^ 3) ^ 2. The similar infixl and infix declarations specify left associativity and nonassociativity, respectively. Precedence levels run from 0 (loosest) to 9 (tightest). If no “fixity” declaration is provided, newly defined operators are left associative by default, and group at level 9. Function application (specified simply via juxtaposition in Haskell) groups tightest of all—effectively at level 10. ■

因为优先级和结合性的规则在不同语言之间差别很大,所以使用多种语言的程序员最好多多使用括号。

Because the rules for precedence and associativity vary so much from one language to another, a programmer who works in several languages is wise to make liberal use of parentheses.

6.1.2 任务

6.1.2 Assignments

在纯函数式语言中,表达式是程序的构建块,计算完全由表达式求值组成。任何单个表达式对整体计算的影响仅限于该表达式为其周围上下文提供的值。复杂的计算使用递归来生成可能无限数量的值、表达式和上下文。

In a purely functional language, expressions are the building blocks of programs, and computation consists entirely of expression evaluation. The effect of any individual expression on the overall computation is limited to the value that expression provides to its surrounding context. Complex computations employ recursion to generate a potentially unbounded number of values, expressions, and contexts.

相比之下,在命令式语言中,计算通常由对内存中变量值的一系列有序更改组成。赋值提供了进行更改的主要方法。每个赋值都采用一对参数:一个值和一个对应将值放入的变量的引用。

In an imperative language, by contrast, computation typically consists of an ordered series of changes to the values of variables in memory. Assignments provide the principal means by which to make the changes. Each assignment takes a pair of arguments: a value and a reference to a variable into which the value should be placed.

一般而言,如果编程语言结构以除返回用于周围上下文的值之外的任何方式影响后续计算(并最终影响程序输出),则该结构具有副作用赋值可能是最基本的副作用:虽然赋值的求值有时可能会产生一个值,但我们真正关心的是它改变了变量的值,从而影响了变量出现的任何后续计算的结果。

In general, a programming language construct is said to have a side effect if it influences subsequent computation (and ultimately program output) in any way other than by returning a value for use in the surrounding context. Assignment is perhaps the most fundamental side effect: while the evaluation of an assignment may sometimes yield a value, what we really care about is the fact that it changes the value of a variable, thereby influencing the result of any later computation in which the variable appears.

许多命令式语言区分表达式和语句,前者总是产生值,可能有副作用,也可能没有副作用,后者只是为了副作用而执行,不返回任何有用的值。鉴于赋值的重要性,命令式编程有时被描述为“通过副作用进行计算”。

Many imperative languages distinguish between expressions, which always produce a value, and may or may not have side effects, and statements, which are executed solely for their side effects, and return no useful value. Given the centrality of assignment, imperative programming is sometimes described as “computing by means of side effects.”

另一个极端是纯函数式语言,它没有副作用。因此,这种语言中表达式的值仅取决于表达式求值的引用环境,而不取决于求值发生。如果表达式在某一时刻产生某个值,则保证在任何时刻都会产生相同的值。用更通俗的话来说,纯函数式语言中的表达式被称为引用透明的。

At the opposite extreme, purely functional languages have no side effects. As a result, the value of an expression in such a language depends only on the referencing environment in which the expression is evaluated, not on the time at which the evaluation occurs. If an expression yields a certain value at one point in time, it is guaranteed to yield the same value at any point in time. In fancier terms, expressions in a purely functional language are said to be referentially transparent.

Haskell 和 Miranda 是纯函数式的。许多其他语言都是混合型的:ML 和 Lisp 主要是函数式的,但为需要它的程序员提供赋值功能。C#、Python 和 Ruby 主要是命令式的,但提供了各种特性(一等函数、多态性、函数值和聚合、垃圾收集、无限范围),允许它们以很大的函数式风格使用。我们将在未来的几个章节中回到函数式编程及其所需的特性,包括6.2.2、6.6、7.3、8.5.3、8.6和11全部内容

Haskell and Miranda are purely functional. Many other languages are mixed: ML and Lisp are mostly functional, but make assignment available to programmers who want it. C#, Python, and Ruby are mostly imperative, but provide a variety of features (first-class functions, polymorphism, functional values and aggregates, garbage collection, unlimited extent) that allow them to be used in a largely functional style. We will return to functional programming, and the features it requires, in several future sections, including 6.2.2, 6.6, 7.3, 8.5.3, 8.6, and all of Chapter 11.

参考和值

References and Values

从表面上看,赋值似乎是一个非常简单的操作。然而,在表面之下,不同的命令式语言在赋值的语义上存在一些微妙但重要的差异。这些差异通常是看不见的,因为它们不会影响简单程序的行为。然而,它们对使用指针的程序有重大影响,我们将在第8.5 节中进一步详细探讨。我们在这里对这些问题进行了介绍。

On the surface, assignment appears to be a very straightforward operation. Below the surface, however, there are some subtle but important differences in the semantics of assignment in different imperative languages. These differences are often invisible, because they do not affect the behavior of simple programs. They have a major impact, however, on programs that use pointers, and will be explored in further detail in Section 8.5. We provide an introduction to the issues here.

例 6.12

Example 6.12

L 值和 r 值

L-values and r-values

考虑以下 C 语言中的赋值:

Consider the following assignments in C:

d=a;

d = a;

a=b+c;

a = b + c;

在第一个语句中,赋值的右边指的是a,我们希望将它放入d中。在第二个语句中,左边指的是a位置,我们希望将bc的和放在此处。这两种解释——值和位置——都是可能的,因为 C 中的变量(与许多其他语言一样)是值的命名容器。我们有时会说像 C 这样的语言使用变量的值模型。由于它们在赋值语句的左边使用,因此表示位置的表达式被称为左值。表示值(可能是存储在某个位置的值)的表达式被称为右值。在变量的值模型下,给定的表达式可以是左值也可以是右值,具体取决于它出现的上下文。■

In the first statement, the right-hand side of the assignment refers to the value of a, which we wish to place into d. In the second statement, the left-hand side refers to the location of a, where we want to put the sum of b and c. Both interpretations—value and location—are possible because a variable in C (as in many other languages) is a named container for a value. We sometimes say that languages like C use a value model of variables. Because of their use on the left-hand side of assignment statements, expressions that denote locations are referred to as l-values. Expressions that denote values (possibly the value stored in a location) are referred to as r-values. Under a value model of variables, a given expression can be either an l-value or an r-value, depending on the context in which it appears. ■

例 6.13

Example 6.13

C 中的 L 值

L-values in C

当然,并非所有表达式都可以是左值,因为并非所有值都有位置,也并非所有名称都是变量。在大多数语言中,如果a是常量的名称,那么说2 + 3 = a或甚至a = 2 + 3都是没有意义的。同样,并非所有左值都是简单名称;左值和右值都可以是复杂的表达式。在 C 中,可以写

Of course, not all expressions can be l-values, because not all values have a location, and not all names are variables. In most languages it makes no sense to say 2 + 3 = a, or even a = 2 + 3, if a is the name of a constant. By the same token, not all l-values are simple names; both l-values and r-values can be complicated expressions. In C one may write

(f(a) + 3)->b[c] = 2;

(f(a)+3)->b[c] = 2;

在这个表达式中,f(a)返回一个指向结构(记录)指针数组中某个元素的指针。赋值将值 2 放入结构字段b的第 c个元素中,该结构由f的返回值指向的数组元素之后的第三个数组元素指向。■

In this expression f(a) returns a pointer to some element of an array of pointers to structures (records). The assignment places the value 2 into the c-th element of field b of the structure pointed at by the third array element after the one to which f's return value points. ■

例 6.14

Example 6.14

C++ 中的 L 值

L-values in C++

在 C++ 中,函数甚至可以返回对结构的引用,而不是指向它的指针,这样就可以编写

In C++ it is even possible for a function to return a reference to a structure, rather than a pointer to it, allowing one to write

g(a).b[c] = 2;

g(a).b[c] = 2;

我们将在第 9.3.1 节中进一步考虑参考文献。

We will consider references further in Section 9.3.1.

例 6.15

Example 6.15

变量作为值和引用

Variables as values and references

语言可以通过使用变量引用模型来更明确地区分左值和右值。这样做的语言包括 Algol 68、Clu、Lisp/Scheme、ML 和 Smalltalk。在这些语言中,变量不是值的命名容器;而是对值的命名引用。以下代码片段在 Pascal 和 Clu 中都是语法有效的:

A language can make the distinction between l-values and r-values more explicit by employing a reference model of variables. Languages that do this include Algol 68, Clu, Lisp/Scheme, ML, and Smalltalk. In these languages, a variable is not a named container for a value; rather, it is a named reference to a value. The following fragment of code is syntactically valid in both Pascal and Clu:

b := 2;

b := 2;

c := b;

c := b;

a:=b+c;

a := b + c;

Pascal 程序员可能会这样描述这段代码:“我们将值 2 放入b中,然后将其复制到c中。然后我们读取这些值,将它们相加,并将结果 4 放入a中。” Clu 程序员会说:“我们让b引用 2,然后让c也引用它。然后我们将这些引用传递给 + 运算符,并让a引用结果,即 4。”

A Pascal programmer might describe this code by saying: “We put the value 2 in b and then copy it into c. We then read these values, add them together, and place the resulting 4 in a.” The Clu programmer would say: “We let b refer to 2 and then let c refer to it also. We then pass these references to the + operator, and let a refer to the result, namely 4.”

图 6.2说明了这两种思维方式。在变量值模型中,任何整数变量都可以包含值 2。在变量引用模型中,任何变量都可以引用(至少在概念上)只有一个2 — 一种柏拉图式的理想。此示例中的实际效果相同,因为整数是不可变的: 2 的值永远不会改变,因此我们无法区分数字 2 的两个副本和对“这个”数字 2 的两个引用。■

These two ways of thinking are illustrated in Figure 6.2. With a value model of variables, any integer variable can contain the value 2. With a reference model of variables, there is (at least conceptually) only one 2—a sort of Platonic Ideal— to which any variable can refer. The practical effect is the same in this example, because integers are immutable: the value of 2 never changes, so we can't tell the difference between two copies of the number 2 and two references to “the” number 2. ■

编号:F06-02-9780124104099
图 6.2 变量的值(左)和参考(右)模型在参考模型下,区分引用同一对象的变量和引用不同对象(且其值(此刻)恰好相等)的变量变得非常重要。

在使用引用模型的语言中,每个变量都是左值。当它出现在需要右值的上下文中时,必须取消引用才能获得它引用的值。在大多数具有引用模型的语言(包括 Clu)中,取消引用是隐式和自动的。在 ML 中,程序员必须使用显式取消引用运算符,以前缀感叹号表示。我们将在8.5.1 节中重新讨论 ML 指针。

In a language that uses the reference model, every variable is an l-value. When it appears in a context that expects an r-value, it must be dereferenced to obtain the value to which it refers. In most languages with a reference model (including Clu), the dereference is implicit and automatic. In ML, the programmer must use an explicit dereference operator, denoted with a prefix exclamation point. We will revisit ML pointers in Section 8.5.1.

如果变量引用的值可以“就地”改变(就像在许多具有链接数据结构的程序中那样),或者变量可以引用恰好具有“相同”值的不同对象,那么变量的值模型和引用模型之间的差异就变得尤为重要(具体而言,它会影响程序的输出和行为)。在后一种情况下,区分引用同一对象的变量和引用不同对象的变量(这些对象的值恰好(此刻)相等)就变得很重要。(正如我们将在第 7.4 节11.3.3节中看到的,Lisp 提供了多种相等概念,以适应这种区别。)我们将在第 8.5 节中进一步讨论变量的值模型和引用模型。

The difference between the value and reference models of variables becomes particularly important (specifically, it can affect program output and behavior) if the values to which variables refer can change “in place,” as they do in many programs with linked data structures, or if it is possible for variables to refer to different objects that happen to have the “same” value. In this latter case it becomes important to distinguish between variables that refer to the same object and variables that refer to different objects whose values happen (at the moment) to be equal. (Lisp, as we shall see in Sections 7.4 and 11.3.3, provides more than one notion of equality, to accommodate this distinction.) We will discuss the value and reference models of variables further in Section 8.5.

Java 对内置类型使用值模型,对用户定义类型(类)使用引用模型。C# 和 Eiffel 允许程序员为每个单独的用户定义类型选择值模型和引用模型。C# 类是引用类型;结构是值类型。

Java uses a value model for built-in types and a reference model for user-defined types (classes). C# and Eiffel allow the programmer to choose between the value and reference models for each individual user-defined type. A C# class is a reference type; a struct is a value type.

拳击

Boxing

例 6.16

Example 6.16

包装器类

Wrapper classes

使用内置类型的值模型的一个缺点是,它们不能统一传递给需要类类型参数的方法。Java 的早期版本要求程序员将内置类型的对象“包装”在相应的预定义类类型中,以便将它们插入到标准容器(集合)类中:

A drawback of using a value model for built-in types is that they can't be passed uniformly to methods that expect class-typed parameters. Early versions of Java required the programmer to “wrap” objects of built-in types inside corresponding predefined class types in order to insert them in standard container (collection) classes:

导入java.util.Hashtable;

import java.util.Hashtable;

哈希表 ht = 新的哈希表();

Hashtable ht = new Hashtable();

Integer N = new Integer(13); //Integer 是一个“包装器”类

Integer N = new Integer(13);     // Integer is a “wrapper” class

ht.put(N,新整数(31));

ht.put(N, new Integer(31));

整数M = (整数) ht.get(N);

Integer M = (Integer) ht.get(N);

int m = M.intValue();

int m = M.intValue();

这里需要包装类,因为Hashtable需要对象类型的参数,而 int 不是对象。■

The wrapper class was needed here because Hashtable expects a parameter of object type, and an int is not an object. ■

设计与实现

Design & Implementation

6.1 实现参考模型

6.1 Implementing the reference model

人们很容易认为变量的引用模型本质上比值模型更昂贵,因为简单的实现需要每次访问时都使用一个间接层。然而,正如我们将在第8.5.1 节中看到的那样,大多数使用引用模型的语言的编译器为了提高效率而使用不可变对象的多个副本,对于简单类型,它们实现的性能与使用值模型时完全相同。

It is tempting to assume that the reference model of variables is inherently more expensive than the value model, since a naive implementation would require a level of indirection on every access. As we shall see in Section 8.5.1, however, most compilers for languages with a reference model use multiple copies of immutable objects for the sake of efficiency, achieving exactly the same performance for simple types that they would with a value model.

例 6.17

Example 6.17

Java 5 和 C# 中的装箱

Boxing in Java 5 and C#

C# 和较新版本的 Java 执行自动装箱拆箱  操作,在许多情况下可避免使用包装器语法:

C# and more recent versions of Java perform automatic boxing and unboxing  operations that avoid the wrapper syntax in many cases:

ht.put(13,31);

ht.put(13, 31);

int m = (整数)ht.get(13);

int m = (Integer) ht.get(13);

这里,Java 编译器创建了隐藏的 Integer 对象来保存值1331,因此它们可以作为引用传递给 put 。仍然需要对返回值进行Integer转换,以确保 13 的哈希表条目确实是整数,而不是浮点数或字符串。我们将在第7.3.1 节中讨论的泛型允许程序员声明一个仅包含整数的表。在 Java 中,这将消除对返回值的转换需求。在 C# 中,它将消除对装箱的需求。■

Here the Java compiler creates hidden Integer objects to hold the values 13 and 31, so they may be passed to put as references. The Integer cast on the return value is still needed, to make sure that the hash table entry for 13 is really an integer and not, say, a floating-point number or string. Generics, which we will consider in Section 7.3.1, allow the programmer to declare a table containing only integers. In Java, this would eliminate the need to cast the return value. In C#, it would eliminate the need for boxing. ■

正交性

Orthogonality

一个常见的设计目标是使语言的各种特性尽可能正交。正交性意味着特性可以以任意组合使用,所有组合都有意义,并且给定特性的含义是一致的,无论它与其他特性组合如何。

A common design goal is to make the various features of a language as orthogonal as possible. Orthogonality means that features can be used in any combination, the combinations all make sense, and the meaning of a given feature is consistent, regardless of the other features with which it is combined.

例 6.18

Example 6.18

Algol 68 中的表达方向

Expression orientation in Algol 68

Algol 68 是首批将正交性作为主要设计目标的语言之一,事实上,此后很少有语言如此重视这一目标。除其他外,Algol 68 被认为是面向表达式的:它没有单独的语句概念。任意表达式可以出现在许多其他语言中需要语句的上下文中,而在其他语言中被视为语句的构造可以出现在表达式中。例如,以下内容在 Algol 68 中有效:

Algol 68 was one of the first languages to make orthogonality a principal design goal, and in fact few languages since have given the goal such weight. Among other things, Algol 68 is said to be expression-oriented: it has no separate notion of statement. Arbitrary expressions can appear in contexts that would call for a statement in many other languages, and constructs that are considered to be statements in other languages can appear within expressions. The following, for example, is valid in Algol 68:

开始

begin

 a := 如果 b < c 则 d 否则 e;

 a := if b < c then d else e;

 a := 开始 f(b); g(c)结束;

 a := begin f(b); g(c) end;

 g(d);

 g(d);

 2 + 3

 2 + 3

结尾

end

这里,if…then…else结构的值要么是其then部分的值,要么是其else部分的值,具体取决于条件的值。第二个赋值语句右侧的“语句列表”的值是其最后一个“语句”的值,即g(c)的返回值。不需要区分过程和函数,因为每个子程序调用都会返回一个值。本例中,g(d)返回的值被丢弃。最后,整个代码片段的值是 5,即 2 加 3 的总和。■

Here the value of the if… then … else construct is either the value of its then part or the value of its else part, depending on the value of the condition. The value of the “statement list” on the right-hand side of the second assignment is the value of its final “statement,” namely the return value of g(c). There is no need to distinguish between procedures and functions, because every subroutine call returns a value. The value returned by g(d) is discarded in this example. Finally, the value of the code fragment as a whole is 5, the sum of 2 and 3. ■

C 采取了一种折中的方法。它区分了语句和表达式,但语句的一种类型是“表达式语句”,它计算表达式的值然后将其丢弃;实际上,这允许表达式出现在大多数其他语言中需要语句的任何上下文中。不幸的是,正如我们在第3.7 节中指出的那样,相反的情况并非如此case:语句通常不能在表达式上下文中使用。C 提供了用于选择和排序的特殊表达式形式。Algol 60 将if…then…else定义为语句和表达式。

C takes an intermediate approach. It distinguishes between statements and expressions, but one of the classes of statement is an “expression statement,” which computes the value of an expression and then throws it away; in effect, this allows an expression to appear in any context that would require a statement in most other languages. Unfortunately, as we noted in Section 3.7, the reverse is not the case: statements cannot in general be used in an expression context. C provides special expression forms for selection and sequencing. Algol 60 defines if… then … else as both a statement and an expression.

例 6.19

Example 6.19

C 条件中的“陷阱”

A “gotcha” in C conditions

Algol 68 和 C 都允许在表达式中赋值。赋值的值只是其右侧的值。大多数 Algol 60 的后代使用 := 标记来表示赋值,而 C 则效仿 Fortran 简单地使用 =。它使用 == 来表示相等性测试(Fortran 使用 . eq.)。此外,在任何需要布尔值的上下文中,C 接受任何可以强制转换为整数的值。它将零解释为 false;任何其他值都为 true。3因此,以下两个构造在 C 中都是有效的(常见的):

Both Algol 68 and C allow assignments within expressions. The value of an assignment is simply the value of its right-hand side. Where most of the descendants of Algol 60 use the := token to represent assignment, C follows Fortran in simply using =. It uses == to represent a test for equality (Fortran uses .eq.). Moreover, in any context that expects a Boolean value, C accepts anything that can be coerced to be an integer. It interprets zero as false; any other value is true.3 As a result, both of the following constructs are valid—common—in C:

如果 (a == b) {

if (a == b) {

 /* 如果 a 等于 b,则执行以下操作 */

 /* do the following if a equals b */

 

 

如果 (a = b) {

if (a = b) {

 /* 将 b 赋值给 a,如果结果非零,则执行以下操作 */

 /* assign b into a and then do the following if the result is nonzero */

 

 

习惯于使用 Ada 或其他语言(其中 = 是相等性测试)的程序员经常会编写上述第二种形式,而第一种形式才是预期的形式。这种错误很难发现。■

Programmers who are accustomed to Ada or some other language in which = is the equality test frequently write the second form above when the first is what is intended. This sort of bug can be very hard to find. ■

尽管 C++ 提供了真正的布尔类型 ( bool ),但它也存在与 C 相同的问题,因为它提供了从数字、指针和枚举类型的自动强制转换。Java 和 C# 通过在布尔上下文中禁止整数来消除此问题。赋值运算符仍然是 =,相等性测试仍然是 ==,但语句if (a = b) … 将生成编译时类型冲突错误,除非ab都是布尔类型。

Though it provides a true Boolean type (bool), C++ shares the problem of C, because it provides automatic coercions from numeric, pointer, and enumeration types. Java and C# eliminate the problem by disallowing integers in Boolean contexts. The assignment operator is still =, and the equality test is still ==, but the statement if (a = b) … will generate a compile-time type clash error unless a and b are both of Boolean type.

组合赋值运算符

Combination Assignment Operators

例 6.20

Example 6.20

更新作业

Updating assignments

由于命令式程序严重依赖副作用,因此必须频繁更新变量。因此,在许多语言中,经常会看到类似这样的语句

Because they rely so heavily on side effects, imperative programs must frequently update a variable. It is thus common in many languages to see statements like

a=a+1;

a = a + 1;

或者更糟的是,

or, worse,

bc[3].d = bc[3].d * e;

b.c[3].d = b.c[3].d * e;

这样的陈述不仅写起来和读起来很麻烦(我们必须仔细检查作业的两边,看看它们是否真的相同),而且导致冗余的地址计算(或者至少在编译的代码改进阶段需要额外的工作来消除冗余)。■

Such statements are not only cumbersome to write and to read (we must examine both sides of the assignment carefully to see if they really are the same), they also result in redundant address calculations (or at least extra work to eliminate the redundancy in the code improvement phase of compilation). ■

例 6.21

Example 6.21

副作用和更新

Side effects and updates

如果地址计算有副作用,那么我们可能需要改写一对语句。考虑以下 C 语言代码:

If the address calculation has a side effect, then we may need to write a pair of statements instead. Consider the following code in C:

void update(int A[], int index_fn(int n)) {

void update(int A[], int index_fn(int n)) {

 int i,j;

 int i, j;

 /* 计算 i */

 /* calculate i */

 

 

 j = 索引_fn(i);

 j = index_fn(i);

 A[j] = A[j] + 1;

 A[j] = A[j] + 1;

}

}

这里我们不能安全地写

Here we cannot safely write

A[索引_fn(i)] = A[索引_fn(i)] + 1;

A[index_fn(i)] = A[index_fn(i)] + 1;

我们必须引入临时变量j,因为我们不知道index_fn是否有副作用。例如,如果它被用来保存已更新元素的日志,那么我们应该确保 update 只调用它一次。■

We have to introduce the temporary variable j because we don't know whether index_fn has a side effect or not. If it is being used, for example, to keep a log of elements that have been updated, then we shall want to make sure that update calls it only once. ■

例 6.22

Example 6.22

赋值运算符

Assignment operators

为了消除冗余地址计算带来的混乱和编译或运行时成本,并避免重复副作用的问题,从 Algol 68 开始,包括 C 及其后代在内的许多语言都提供了所谓的赋值运算符来更新变量。使用赋值运算符,示例 6.20中的语句可以写成如下形式:

To eliminate the clutter and compile- or run-time cost of redundant address calculations, and to avoid the issue of repeated side effects, many languages, beginning with Algol 68, and including C and its descendants, provide so-called assignment operators to update a variable. Using assignment operators, the statements in Example 6.20 can be written as follows:

a += 1;

a += 1;

bc[3].d *=e;

b.c[3].d *= e;

更新函数中的两个赋值可以替换为

and the two assignments in the update function can be replaced with

A[index_fn(i)] += 1;

A[index_fn(i)] += 1;

除了看起来更简洁之外,赋值运算符形式还能保证地址计算和任何副作用只发生一次。■

In addition to being aesthetically cleaner, the assignment operator form guarantees that the address calculation and any side effects happen only once. ■

例 6.23

Example 6.23

前缀和后缀增加/减少

Prefix and postfix inc/dec

如图6.1所示,C 提供了 10 个不同的赋值运算符,每个赋值运算符对应一个二进制算术运算符和位运算符。C 还提供了前缀和后缀递增和递减运算。这些运算允许更新代码更加简单:

As shown in Figure 6.1, C provides 10 different assignment operators, one for each of its binary arithmetic and bit-wise operators. C also provides prefix and postfix increment and decrement operations. These allow even simpler code in update:

A[index_fn(i)]++;

A[index_fn(i)]++;

或者

or

++A[index_fn(i)];

++A[index_fn(i)];

更重要的是,增量和减量运算符为使用索引或指针遍历数组的代码提供了优雅的语法:

More significantly, increment and decrement operators provide elegant syntax for code that uses an index or a pointer to traverse an array:

A[−−i]=b;

A[−−i] = b;

*p++ = *q++;

*p++ = *q++;

当 ++ 或 −− 运算符作为表达式的前缀时,它会先增加或减少其操作数,然后再向周围上下文提供值。在后缀形式中,++ 或 −− 会在提供值之后更新其操作数。如果i3,且pq指向一对数组的初始元素,则b将被分配到A[2](而不是A[3]),而第二次分配将复制数组的初始元素(而不是第二个元素)。■

When prefixed to an expression, the ++ or −− operator increments or decrements its operand before providing a value to the surrounding context. In the postfix form, ++ or −− updates its operand after providing a value. If i is 3 and p and q point to the initial elements of a pair of arrays, then b will be assigned into A[2] (not A[3]), and the second assignment will copy the initial elements of the arrays (not the second elements). ■

例 6.24

Example 6.24

后缀 inc/dec 的优点

Advantages of postfix inc/dec

++ 和 −− 的前缀形式是 += 和 −= 的语法糖。我们可以写成

The prefix forms of ++ and −− are syntactic sugar for += and −=. We could have written

A[i -= 1] = b;

A [i -= 1] = b;

以上。后缀形式不是语法糖。为了获得类似于上述第二个语句的效果,我们需要一个辅助变量和大量额外的符号:

above. The postfix forms are not syntactic sugar. To obtain an effect similar to the second statement above we would need an auxiliary variable and a lot of extra notation:

*(t = p,p += 1,t) = *(t = q,q += 1,t);

*(t = p, p += 1, t) = *(t = q, q += 1, t);

赋值运算符 (+=, −=) 和递增和递减运算符 (++, −−) 在应用于 C 中的指针时(假设这些指针指向一个数组),都会“正确”地执行操作。如果p指向数组的元素i ,其中每个元素占用n 个字节(包括对齐所需的任何字节,如第 C-5.1 节所述),则p += 3指向元素i + 3,即内存中 3n 个字节之后的位置。我们将在第 8.5.1 节中更详细地讨论 C 中的指针和数组。

Both the assignment operators (+=, −=) and the increment and decrement operators (++, −−) do “the right thing” when applied to pointers in C (assuming those pointers point into an array). If p points to element i of an array, where each element occupies n bytes (including any bytes required for alignment, as discussed in Section C-5.1), then p += 3 points to element i + 3, 3n bytes later in memory. We will discuss pointers and arrays in C in more detail in Section 8.5.1.

多路分配

Multiway Assignment

例 6.25

Example 6.25

简单多路分配

Simple multiway assignment

我们已经看到,赋值的右结合性(在允许在表达式中赋值的语言中)允许我们写出a = b = c这样的代码。在包括 Clu、ML、Perl、Python 和 Ruby 在内的多种语言中,也可以这样写

We have already seen that the right associativity of assignment (in languages that allow assignment in expressions) allows one to write things like a = b = c. In several languages, including Clu, ML, Perl, Python, and Ruby, it is also possible to write

a,b=c,d;

a, b = c, d;

这里右边的逗号不是C的排序运算符。相反,它用于定义一个由多个 r 值组成的表达式或元组。左边的逗号运算符生成一个 l 值元组。赋值的效果是将c复制到a中d复制到b中。4 ■

Here the comma in the right-hand side is not the sequencing operator of C. Rather, it serves to define an expression, or tuple, consisting of multiple r-values. The comma operator on the left-hand side produces a tuple of l-values. The effect of the assignment is to copy c into a and d into b.4

例 6.26

Example 6.26

多路分配的优点

Advantages of multiway assignment

虽然我们也可以很容易地写

While we could just as easily have written

a = c; b = d;

a = c; b = d;

多路(元组)赋值允许我们写类似的东西

the multiway (tuple) assignment allows us to write things like

a, b = b, a; (* 交换 a 和 b *)

a, b = b, a;    (* swap a and b *)

否则将需要辅助变量。此外,多路赋值允许函数返回元组以及单个值:

which would otherwise require auxiliary variables. Moreover, multiway assignment allows functions to return tuples, as well as single values:

a,b,c = foo(d,e,f);

a, b, c = foo(d, e, f);

这种表示法消除了大多数编程语言中函数的不对称性(非正交性),这些语言允许任意数量的参数,但只能返回一个。■

This notation eliminates the asymmetry (nonorthogonality) of functions in most programming languages, which allow an arbitrary number of arguments, but only a single return. ■

06-01-9780124104099检查你的理解

Check Your Understanding

1. 说出八种主要的控制流机制类别。

1. Name eight major categories of control-flow mechanisms.

2. 运算符与其他类型的函数有何区别?

2. What distinguishes operators from other sorts of functions?

3.解释 前缀、中缀后缀表示法之间的区别。什么是剑桥波兰表示法?说出两种使用后缀表示法的编程语言。

3. Explain the difference between prefix, infix, and postfix notation. What is Cambridge Polish notation? Name two programming languages that use postfix notation.

4. 为什么在 Postscript 或 Forth 中不会出现结合性和优先级问题?

4. Why don't issues of associativity and precedence arise in Postscript or Forth?

5.表达式的指称 透明是什么意思?

5. What does it mean for an expression to be referentially transparent?

6. 变量的价值模型和变量的参考模型有什么区别?为什么这种区别很重要?

6. What is the difference between a value model of variables and a reference model of variables? Why is the distinction important?

7. 什么是左值?什么是右值

7. What is an l-value? An r-value?

8.为什么 在具有变量参考模型的语言的实现中,可变值不可变值之间的区别很重要?

8. Why is the distinction between mutable and immutable values important in the implementation of a language with a reference model of variables?

9. 在编程语言设计的背景下定义正交性。

9. Define orthogonality in the context of programming language design.

10. 语句和表达式有什么区别?语言以表达式为导向意味着什么?

10. What is the difference between a statement and an expression? What does it mean for a language to be expression-oriented?

11. 使用赋值运算符更新变量与使用变量同时出现在左侧和右侧的常规赋值相比有哪些优势?

11. What are the advantages of updating a variable with an assignment operator, rather than with a regular assignment in which the variable appears on both the left- and right-hand sides?

6.1.3 初始化

6.1.3 Initialization

由于命令式语言已经提供了设置变量值的结构(赋值语句),因此它们并不总是提供在变量声明中指定变量初始值的方法。不过,有以下几个原因可以说明为什么这样的初始值可能很有用:

Because they already provide a construct (the assignment statement) to set the value of a variable, imperative languages do not always provide a means of specifying an initial value for a variable in its declaration. There are several reasons, however, why such initial values may be useful:

1.如图 3.3所示,子程序的局部静态变量需要初始值才能使用。

1. As suggested in Figure 3.3, a static variable that is local to a subroutine needs an initial value in order to be useful.

2. 对于任何静态分配的变量,声明中指定的初始值可以由编译器在全局内存中预先分配,从而避免在运行时分配初始值的成本。

2. For any statically allocated variable, an initial value that is specified in the declaration can be preallocated in global memory by the compiler, avoiding the cost of assigning an initial value at run time.

3. 意外使用未初始化的变量是最常见的编程错误之一。防止此类错误(或至少确保错误行为可重复)的最简单方法之一是在首次声明每个变量时为其赋值。

3. Accidental use of an uninitialized variable is one of the most common programming errors. One of the easiest ways to prevent such errors (or at least ensure that erroneous behavior is repeatable) is to give every variable a value when it is first declared.

大多数语言允许在声明中初始化内置类型的变量。更完整、更正交的初始化方法需要聚合符号:用户定义的复合类型的结构化值。聚合可以在多种语言中找到,包括 C、C++、Ada、Fortran 90 和 ML;我们将在第7.1.3 节中进一步讨论它们。

Most languages allow variables of built-in types to be initialized in their declarations. A more complete and orthogonal approach to initialization requires a notation for aggregates: built-up structured values of user-defined composite types. Aggregates can be found in several languages, including C, C++, Ada, Fortran 90, and ML; we will discuss them further in Section 7.1.3.

需要强调的是,初始化仅对静态分配的变量节省时间。运行时在堆栈或堆中分配的变量必须在运行时初始化。5值得注意的是,使用未初始化变量的问题不仅发生在细化之后,还可能由于任何破坏变量值而不提供新值的操作而发生。最常见的两种此类操作是通过指针引用的对象的显式释放和变体记录的标签修改。我们将分别在第 8.5 节C-8.1.3节中进一步讨论这些操作。

It should be emphasized that initialization saves time only for variables that are statically allocated. Variables allocated in the stack or heap at run time must be initialized at run time.5 It is also worth noting that the problem of using an uninitialized variable occurs not only after elaboration, but also as a result of any operation that destroys a variable's value without providing a new one. Two of the most common such operations are explicit deallocation of an object referenced through a pointer and modification of the tag of a variant record. We will consider these operations further in Sections 8.5 and C-8.1.3, respectively.

如果在变量声明中未明确赋予其初始值,则语言可以指定默认值。例如,在 C 语言中,程序员未提供初始值的静态分配变量保证在内存中表示为好像它们已被初始化为零。对于大多数机器上的大多数类型,这都是一串零位,允许语言实现利用大多数操作系统(出于安全原因)用零填充新分配的内存这一事实。零初始化以递归方式应用于用户定义复合类型变量的子组件。Java 和 C# 为所有类类型对象的字段提供类似的保证,而不仅仅是静态分配的字段。大多数脚本语言为所有类型的所有变量都提供默认初始值,无论其范围或生存期如何。

If a variable is not given an initial value explicitly in its declaration, the language may specify a default value. In C, for example, statically allocated variables for which the programmer does not provide an initial value are guaranteed to be represented in memory as if they had been initialized to zero. For most types on most machines, this is a string of zero bits, allowing the language implementation to exploit the fact that most operating systems (for security reasons) fill newly allocated memory with zeros. Zero-initialization applies recursively to the subcomponents of variables of user-defined composite types. Java and C# provide a similar guarantee for the fields of all class-typed objects, not just those that are statically allocated. Most scripting languages provide a default initial value for all variables, of all types, regardless of scope or lifetime.

动态检查

Dynamic Checks

语言或实现可以选择将未初始化变量的使用定义为动态语义错误,并在运行时捕获这些错误,而不是为每个未初始化变量赋予默认值。语义检查的优点是它们通常可以识别出因默认值的存在而被掩盖或变得更加隐蔽的程序错误。有了适当的硬件支持,未初始化变量检查甚至可以像默认值一样便宜,至少对于某些类型而言。具体来说,依赖于 IEEE 浮点算术标准的编译器可以用信号NaN值填充未初始化的浮点数,如第 C-5.2.2 节所述。任何在计算中使用此类值的尝试都将导致硬件中断,语言实现可能会捕获该中断(在操作系统的帮助下),并使用它来触发语义错误消息。

Instead of giving every uninitialized variable a default value, a language or implementation can choose to define the use of an uninitialized variable as a dynamic semantic error, and can catch these errors at run time. The advantage of the semantic checks is that they will often identify a program bug that is masked or made more subtle by the presence of a default value. With appropriate hardware support, uninitialized variable checks can even be as cheap as default values, at least for certain types. In particular, a compiler that relies on the IEEE standard for floating-point arithmetic can fill uninitialized floating-point numbers with a signaling NaN value, as discussed in Section C-5.2.2. Any attempt to use such a value in a computation will result in a hardware interrupt, which the language implementation may catch (with a little help from the operating system), and use to trigger a semantic error message.

不幸的是,对于大多数机器上的大多数类型,在运行时捕获未初始化变量的所有使用的成本要高得多。如果变量在内存中的表示的每个可能的位模式都指定某个合法值(通常情况如此),则必须在某处分配额外的空间来保存已初始化/未初始化标志。此标志必须在阐述时设置为“未初始化”,在分配时设置为“已初始化”。每次使用时,或者至少在代码改进者无法证明是冗余的每次使用时,也必须检查它(通过额外的代码)。

For most types on most machines, unfortunately, the costs of catching all uses of an uninitialized variable at run time are considerably higher. If every possible bit pattern of the variable's representation in memory designates some legitimate value (and this is often the case), then extra space must be allocated somewhere to hold an initialized/uninitialized flag. This flag must be set to “uninitialized” at elaboration time and to “initialized” at assignment time. It must also be checked (by extra code) at every use, or at least at every use that the code improver is unable to prove is redundant.

明确分配

Definite Assignment

例 6.27

Example 6.27

明确分配禁止的程序

Programs outlawed by definite assignment

对于方法的局部变量,Java 和 C# 定义了明确赋值的概念,以防止使用未初始化的变量。此概念基于程序的控制流,可以由编译器静态检查。粗略地说,表达式的每个可能控制路径都必须为该表达式中的每个变量赋值。这是一条保守的规则;它有时会禁止永远不会使用未初始化变量的程序。在 Java 中:

For local variables of methods, Java and C# define a notion of definite assignment that precludes the use of uninitialized variables. This notion is based on the control flow of the program, and can be statically checked by the compiler. Roughly speaking, every possible control path to an expression must assign a value to every variable in that expression. This is a conservative rule; it can sometimes prohibit programs that would never actually use an uninitialized variable. In Java:

int 我;

int i;

int j = 3;

int j = 3;

如果(j> 0){

if (j > 0) {

 我=2;

 i = 2;

}

}

… // 这里没有对 j 进行赋值

…    // no assignments to j in here

如果(j> 0){

if (j > 0) {

 System.out.println(i); //错误:“i 可能尚未初始化”

 System.out.println(i); // error: “i might not have been initialized”

}

}

尽管人类可能会推断只有当 i 先前已被赋值时才会使用,但这种判断在一般情况下是不可判定的,而且编译器不会尝试这样做。■

While a human being might reason that i will be used only when it has previously been given a value, such determinations are undecidable in the general case, and the compiler does not attempt them. ■

构造函数

Constructors

许多面向对象语言(其中包括 Java 和 C#)允许程序员定义类型,即使声明中未指定初始值,动态分配的变量的初始化也会自动发生。一些语言(尤其是 C++)还仔细区分了初始化和赋值。初始化被解释为对变量类型的构造函数的调用,初始值作为参数。在没有强制的情况下,赋值被解释为对类型的赋值运算符的调用,或者,如果没有定义,则被解释为对赋值右侧值的简单逐位复制。初始化和赋值之间的区别对于执行自己的存储管理的用户定义的抽象数据类型尤其重要。一个典型的例子是可变长度的字符串。对这样的字符串进行赋值通常必须释放字符串旧值所占用的空间,然后再为新值分配空间。字符串的初始化必须简单地分配空间。使用非平凡值进行初始化通常比默认初始化后再赋值更便宜,因为它避免了释放为默认值分配的空间。我们将在第 10.3.2 节中再次讨论这个问题。

Many object-oriented languages (Java and C# among them) allow the programmer to define types for which initialization of dynamically allocated variables occurs automatically, even when no initial value is specified in the declaration. Some—notably C++—also distinguish carefully between initialization and assignment. Initialization is interpreted as a call to a constructor function for the variable's type, with the initial value as an argument. In the absence of coercion, assignment is interpreted as a call to the type's assignment operator or, if none has been defined, as a simple bit-wise copy of the value on the assignment's right-hand side. The distinction between initialization and assignment is particularly important for user-defined abstract data types that perform their own storage management. A typical example occurs in variable-length character strings. An assignment to such a string must generally deallocate the space consumed by the old value of the string before allocating space for the new value. An initialization of the string must simply allocate space. Initialization with a nontrivial value is generally cheaper than default initialization followed by assignment, because it avoids deallocation of the space allocated for the default value. We will return to this issue in Section 10.3.2.

Java 和 C# 都不区分初始化和赋值:可以在声明中给出初始值,但这与紧接着的赋值相同。Java 对用户定义对象类型的所有变量使用引用模型,并提供自动存储回收,因此赋值永远不会复制值。C# 允许程序员在需要时指定值模型(在这种情况下赋值会复制值),但其他方面与 Java 相似。

Neither Java nor C# distinguishes between initialization and assignment: an initial value can be given in a declaration, but this is the same as an immediate subsequent assignment. Java uses a reference model for all variables of user-defined object types, and provides for automatic storage reclamation, so assignment never copies values. C# allows the programmer to specify a value model when desired (in which case assignment does copy values), but otherwise mirrors Java.

6.1.4 表达式内的排序

6.1.4 Ordering within Expressions

例 6.28

Example 6.28

不确定的顺序

Indeterminate ordering

虽然优先级和结合性规则定义了二元中缀运算符在表达式中的应用顺序,但它们并未指定给定运算符的操作数的求值顺序。例如,在表达式中

While precedence and associativity rules define the order in which binary infix operators are applied within an expression, they do not specify the order in which the operands of a given operator are evaluated. For example, in the expression

a − f (b) − c * d

a − f (b) − c * d

我们从结合律知道,在执行第二次减法之前, f(b)将从 a 中减去,并且我们从优先级知道第二次减法的右操作数将是c * d的结果,而不仅仅是c,但如果没有其他信息,我们不知道a − f (b)是在c * d之前还是之后进行评估。同样,在具有多个参数的子程序调用中

we know from associativity that f(b) will be subtracted from a before performing the second subtraction, and we know from precedence that the right operand of that second subtraction will be the result of c * d, rather than merely c, but without additional information we do not know whether a − f (b) will be evaluated before or after c * d. Similarly, in a subroutine call with multiple arguments

f(a, g(b), h(c))

f(a, g(b), h(c))

我们不知道参数的求值顺序。■

we do not know the order in which the arguments will be evaluated. ■

该顺序之所以重要,主要有两个原因:

There are two main reasons why the order can be important:

例 6.29

Example 6.29

取决于顺序的值

A value that depends on ordering

1. 副作用:f(b) 可以修改 d,那么a − f(b) − c * d的值将取决于先执行减法还是先执行乘法。类似地,如果g(b)可以修改a和/或c ,那么传递给f(a, g(b), h(c)) 的值将取决于参数的求值顺序。■

1. Side effects: If f(b) may modify d, then the value of a − f(b) − c * d will depend on whether the first subtraction or the multiplication is performed first. Similarly, if g(b) may modify a and/or c, then the values passed to f(a, g(b), h(c)) will depend on the order in which the arguments are evaluated. ■

例 6.30

Example 6.30

依赖于排序的优化

An optimization that depends on ordering

2. 代码改进:子表达式的求值顺序对寄存器分配和指令调度都有影响。在表达式a * b + f(c)中,在求值a * b之前调用f可能是可取的,因为如果先计算乘积,则需要在调用f期间保存乘积,而f可能希望使用所有可以轻松保存乘积的寄存器。类似地,考虑以下序列

2. Code improvement: The order of evaluation of subexpressions has an impact on both register allocation and instruction scheduling. In the expression a * b + f(c), it is probably desirable to call f before evaluating a * b, because the product, if calculated first, would need to be saved during the call to f, and f might want to use all the registers in which it might easily be saved. In a similar vein, consider the sequence

a := B[i];

a := B[i];

c:=a*2+d*3;

c := a * 2 + d * 3;

在按顺序执行的处理器上,在评估a * 2之前评估d * 3可能是可取的,因为前一个语句a := B[i]需要从内存中加载一个值。由于加载速度很慢,如果处理器试图在下一条指令(或在许多机器上甚至是接下来的几条指令)中使用 a 的值,它将不得不等待。如果它改为执行一些不相关的操作(即评估d * 3),那么加载可以与其他计算并行进行。■

On an in-order processor, it is probably desirable to evaluate d * 3 before evaluating a * 2, because the previous statement, a := B[i], will need to load a value from memory. Because loads are slow, if the processor attempts to use the value of a in the next instruction (or even the next few instructions on many machines), it will have to wait. If it does something unrelated instead (i.e., evaluate d * 3), then the load can proceed in parallel with other computation. ■

由于代码改进的重要性,大多数语言手册都说操作数和参数的求值顺序是未定义的。(Java 和 C# 在这方面很不寻常:它们要求从左到右进行求值。)在没有强制顺序的情况下,编译器可以选择任何可能导致更快代码的顺序。

Because of the importance of code improvement, most language manuals say that the order of evaluation of operands and arguments is undefined. (Java and C# are unusual in this regard: they require left-to-right evaluation.) In the absence of an enforced order, the compiler can choose whatever order is likely to result in faster code.

设计与实现

Design & Implementation

6.2 安全性与性能

6.2 Safety versus performance

在比较 C++ 和 Java 时,一个反复出现的主题是后者愿意接受额外的运行时成本,以获得更清晰的语义或更高的可靠性。明确赋值就是一个例子:它可能会迫使程序员在某些代码路径上执行“不必要的”初始化,但这样做可以避免其他语言中缺少初始化而可能出现的许多细微错误。同样,Java 规范要求自动垃圾收集,其用户定义类型的参考模型强制大多数对象分配在堆中。正如我们将在后面的章节中看到的那样,Java 还要求动态绑定所有方法调用,并在运行时检查越界数组引用、类型冲突和其他动态语义错误。聪明的编译器可以在某些常见情况下降低或消除这些要求的成本,但在大多数情况下,Java 设计反映了一种进化转变,不再将性能作为首要的设计目标。

A recurring theme in any comparison between C++ and Java is the latter's willingness to accept additional run-time cost in order to obtain cleaner semantics or increased reliability. Definite assignment is one example: it may force the programmer to perform “unnecessary” initializations on certain code paths, but in so doing it avoids the many subtle errors that can arise from missing initialization in other languages. Similarly, the Java specification mandates automatic garbage collection, and its reference model of user-defined types forces most objects to be allocated in the heap. As we shall see in future chapters, Java also requires both dynamic binding of all method invocations and run-time checks for out-of-bounds array references, type clashes, and other dynamic semantic errors. Clever compilers can reduce or eliminate the cost of these requirements in certain common cases, but for the most part the Java design reflects an evolutionary shift away from performance as the overriding design goal.

应用数学恒等式

Applying Mathematical Identities

例 6.31

Example 6.31

优化和数学“定律”

Optimization and mathematical “laws”

某些语言实现(例如 Fortran 的方言)允许编译器重新排列涉及数学抽象为交换、结合和/或分配的运算符的表达式,以便生成更快的代码。考虑以下 Fortran 片段:

Some language implementations (e.g., for dialects of Fortran) allow the compiler to rearrange expressions involving operators whose mathematical abstractions are commutative, associative, and/or distributive, in order to generate faster code. Consider the following Fortran fragment:

a=b+c

a = b + c

d=c+e+b

d = c + e + b

有些编译器会将其重新排列为

Some compilers will rearrange this as

a = b + cd = b + c + e

a = b + c d = b + c + e

然后,他们可以识别第一和第二个语句中的公共子表达式,并生成相当于

They can then recognize the common subexpression in the first and second statements, and generate code equivalent to

a=b+c

a = b + c

d=a+e

d = a + e

相似地,

Similarly,

a = b/c/d

a = b/c/d

e = f/d/c

e = f/d/c

可以重新排列为

may be rearranged as

t = c * d

t = c * d

a=b/t

a = b/t

e = f/t

e = f/t

例 6.32

Example 6.32

溢出和算术“恒等式”

Overflow and arithmetic “identities”

不幸的是,数学算术遵循各种交换律、结合律和分配律,而计算机算术却不那么有序。问题在于计算机中的数字精度有限。假设abc都是介于 20 亿和 30 亿之间的整数。使用 32 位算术,表达式b − c + d可以安全地从左到右求值(2 32略小于 43 亿)。但是,如果编译器尝试将此表达式重组为b + d − c(例如,为了延迟使用c),则会发生算术溢出。尽管我们从数学上可以直观地看出,这种重组是不安全的。■

Unfortunately, while mathematical arithmetic obeys a variety of commutative, associative, and distributive laws, computer arithmetic is not as orderly. The problem is that numbers in a computer are of limited precision. Suppose a, b, and c are all integers between two billion and three billion. With 32-bit arithmetic, the expression b − c + d can be evaluated safely left-to-right (232 is a little less than 4.3 billion). If the compiler attempts to reorganize this expression as b + d − c, however (e.g., in order to delay its use of c), then arithmetic overflow will occur. Despite our intuition from math, this reorganization is unsafe. ■

设计与实现

Design & Implementation

6.3 评估顺序

6.3 Evaluation order

表达式求值在语义和实现之间呈现出一种艰难的权衡。为了减少意外,大多数语言定义都要求编译器(如果它要重新排序表达式)遵守括号强加的任何顺序。因此,程序员可以在需要时使用括号来防止应用算术“恒等式”。对于操作数和参数的求值顺序,没有类似的保证。因此,编写表达式时,如果对一个操作数或参数的求值的副作用会影响另一个操作数或参数的值,则是不明智的。正如我们将在第 6.3 节中看到的,某些语言(尤其是 Euclid 和 Turing)禁止此类副作用。

Expression evaluation presents a difficult tradeoff between semantics and implementation. To limit surprises, most language definitions require the compiler, if it ever reorders expressions, to respect any ordering imposed by parentheses. The programmer can therefore use parentheses to prevent the application of arithmetic “identities” when desired. No similar guarantee exists with respect to the order of evaluation of operands and arguments. It is therefore unwise to write expressions in which a side effect of evaluating one operand or argument can affect the value of another. As we shall see in Section 6.3, some languages, notably Euclid and Turing, outlaw such side effects.

许多语言(包括 Pascal 及其大多数后代)都提供动态语义检查来检测算术溢出。在某些实现中,可以禁用这些检查以消除其运行时开销。在 C 和 C++ 中,算术溢出的影响取决于实现。在 Java 中,它定义明确:语言定义指定所有数字类型的大小,并要求二进制补码整数和 IEEE 浮点运算。在 C# 中,程序员可以通过用 checked 或 unchecked 关键字标记表达式或语句来明确请求检查的存在或不存在。在完全不同的方面,Scheme、Common Lisp 和几种脚本语言对整数的大小没有先验限制;根据需要分配空间来保存超大值。

Many languages, including Pascal and most of its descendants, provide dynamic semantic checks to detect arithmetic overflow. In some implementations these checks can be disabled to eliminate their run-time overhead. In C and C++, the effect of arithmetic overflow is implementation-dependent. In Java, it is well defined: the language definition specifies the size of all numeric types, and requires two's complement integer and IEEE floating-point arithmetic. In C#, the programmer can explicitly request the presence or absence of checks by tagging an expression or statement with the checked or unchecked keyword. In a completely different vein, Scheme, Common Lisp, and several scripting languages place no a priori limit on the size of integers; space is allocated to hold extra-large values on demand.

例 6.33

Example 6.33

重新排序和数值稳定性

Reordering and numerical stability

即使没有溢出,浮点运算的有限精度也会导致“相同”表达式的不同排列产生明显不同的结果,而且这种结果似乎是看不见的。单精度 IEEE 浮点数用 1 位表示符号,8 位表示指数(2 的幂),23 位表示尾数。在这种表示下,如果 |log 2 ( a / b )| > 23,则a + b必然会导致信息丢失。因此,如果b = -c,并且a的幅度很小,而bc的幅度很大,则a + b + c可能看起来是零,而不是a。类似地,像 0.1 这样的数字无法精确表示,因为它的二进制表示是“循环小数”:0.0001001001...对于某些 x 值,即使0.1x 的数量级相同,(0.1 + x) * 10.01.0 + (x * 10.0) 也可能相差多达 25% 。■

Even in the absence of overflow, the limited precision of floating-point arithmetic can cause different arrangements of the “same” expression to produce significantly different results, invisibly. Single-precision IEEE floating-point numbers devote one bit to the sign, eight bits to the exponent (power of two), and 23 bits to the mantissa. Under this representation, a + b is guaranteed to result in a loss of information if |log2(a/b)| > 23. Thus if b = -c, then a + b + c may appear to be zero, instead of a, if the magnitude of a is small, while the magnitudes of b and c are large. In a similar vein, a number like 0.1 cannot be represented precisely, because its binary representation is a “repeating decimal”: 0.0001001001…. For certain values of x, (0.1 + x) * 10.0 and 1.0 + (x * 10.0) can differ by as much as 25%, even when 0.1 and x are of the same magnitude. ■

6.1.5 短路评估

6.1.5 Short-Circuit Evaluation

例 6.34

Example 6.34

短路表达式

Short-circuited expressions

布尔表达式为改进代码和提高可读性提供了一个特殊而重要的机会。考虑表达式 ( a < b ) 和 ( b < c )。如果a大于b,那么检查b是否小于c实际上毫无意义;我们知道整个表达式一定为假。类似地,在表达式(a > b)(b > c)中,如果 a 确实大于b ,那么检查b是否大于c毫无意义;我们知道整个表达式一定为真。当可以根据前半部分确定整体值时,执行布尔表达式短路求值的编译器将生成跳过这两个计算的后半部分的代码。■

Boolean expressions provide a special and important opportunity for code improvement and increased readability. Consider the expression (a < b) and (b < c). If a is greater than b, there is really no point in checking to see whether b is less than c; we know the overall expression must be false. Similarly, in the expression (a > b) or (b > c), if a is indeed greater than b there is no point in checking to see whether b is greater than c; we know the overall expression must be true. A compiler that performs short-circuit evaluation of Boolean expressions will generate code that skips the second half of both of these computations when the overall value can be determined from the first half. ■

例 6.35

Example 6.35

通过短路节省时间

Saving time with short-circuiting

在某些情况下,短路评估可以节省大量时间:

Short-circuit evaluation can save significant amounts of time in certain situations:

如果(非常不可能的条件 && 非常昂贵的函数())…

if (very_unlikely_condition && very_expensive_function()) …

例 6.36

Example 6.36

短路指针追逐

Short-circuit pointer chasing

但时间并不是唯一的考虑因素,甚至不是最重要的因素。短路改变了布尔表达式的语义。例如,在 C 语言中,可以使用以下代码在列表中搜索元素:

But time is not the only consideration, or even the most important. Short-circuiting changes the semantics of Boolean expressions. In C, for example, one can use the following code to search for an element in a list:

p = 我的列表;

p = my_list;

当(p && p->key != val)

while (p && p->key != val)

 p = p->下一步;

 p = p->next;

C 将其&&||运算符短路,并使用零表示 null 和 false,因此当且仅当p非空时,才会访问p->key。Pascal中语法类似的代码不起作用,因为 Pascal 不会短路andor

C short-circuits its && and || operators, and uses zero for both null and false, so p->key will be accessed if and only if p is non-null. The syntactically similar code in Pascal does not work, because Pascal does not short-circuit and and or:

p:=我的列表;

p := my_list;

while (p <> nil) and (p^.key <> val) do (* 哎哟!*)

while (p <> nil) and (p^.key <> val) do  (* ouch! *)

 p:=p^.下一个;

 p := p^.next;

这里,两个<>关系都将在将它们的结果进行与运算之前进行求值。在搜索失败后,p将为 nil,并且尝试访问p^.key将导致运行时(动态语义)错误,编译器可能会生成代码来捕获该错误,也可能没有。为了避免这种情况,Pascal 程序员必须引入辅助布尔变量和额外的嵌套级别:

Here both of the <> relations will be evaluated before and-ing their results together. At the end of an unsuccessful search, p will be nil, and the attempt to access p^.key will be a run-time (dynamic semantic) error, which the compiler may or may not have generated code to catch. To avoid this situation, the Pascal programmer must introduce an auxiliary Boolean variable and an extra level of nesting:

p:=我的列表;

p := my_list;

仍然搜索 := true;

still_searching := true;

while still_searching 执行

while still_searching do

 如果 p = nil 那么

 if p = nil then

  仍然搜索 := false

  still_searching := false

 否则,如果 p^.key = val 则

 else if p^.key = val then

  仍然搜索 := false

  still_searching := false

 别的

 else

  p := p^.next;

  p := p^.next;

例 6.37

Example 6.37

短路和其他错误

Short-circuiting and other errors

短路求值也可以用来避免下标越界:

Short-circuit evaluation can also be used to avoid out-of-bound subscripts:

const int MAX = 10;

const int MAX = 10;

int A[MAX]; /* 索引从 0 到 9 */

int A[MAX];    /* indices from 0 to 9 */

如果 (i >= 0 && i < MAX && A[i] > foo) …

if (i >= 0 && i < MAX && A[i] > foo) …

除以零:

division by zero:

如果 (d == 0 || n/d < 阈值) …

if (d == 0 || n/d < threshold) …

以及其他各种错误。■

and various other errors. ■

例 6.38

Example 6.38

可选短路

Optional short-circuiting

但是,有些情况下短路可能并不合适。特别是,如果表达式E1E2都有副作用,我们可能希望连词E1 and E2(以及E1 or E2)同时求值两半(练习 6.12 )。为了适应这种情况,同时仍然允许在示例 6.356.37等场景中进行短路求值,一些语言同时包含常规短路布尔运算符。例如,在 Ada 中,常规布尔运算符是andor;短路版本是两个字的组合and thenor else

There are situations, however, in which short circuiting may not be appropriate. In particular, if expressions E1 and E2 both have side effects, we may want the conjunction E1 and E2 (and likewise E1 or E2) to evaluate both halves (Exercise 6.12). To accommodate such situations while still allowing short-circuit evaluation in scenarios like those of Examples 6.35 through 6.37, a few languages include both regular and short-circuit Boolean operators. In Ada, for example, the regular Boolean operators are and and or; the short-circuit versions are the two-word combinations and then and or else:

found_it := p /= null 然后 p.key = val;

found_it := p /= null and then p.key = val;

如果 d = 0.0 或 n/d < 阈值则…

if d = 0.0 or else n/d < threshold then …

(Ada 使用 /= 表示“不等于”。)在 C 语言中,当位运算符&|的参数是逻辑值(零或一)时,它们可以用作&&||的非短路替代。■

(Ada uses /= for “not equal.”) In C, the bit-wise & and | operators can be used as non-short-circuiting alternatives to && and || when their arguments are logical (zero or one) values. ■

如果我们将 and 和 or 视为二元运算符,那么短路可以被视为延迟或惰性求值的一个例子:操作数未经求值就“传递”。在内部,运算符在任何情况下都会求值第一个操作数,仅在需要时才求值第二个操作数。在 Algol 68 等允许在表达式中使用任意控制流构造的语言中,可以使用if…then…else明确指定条件求值;参见练习 6.13

If we think of and and or as binary operators, short circuiting can be considered an example of delayed or lazy evaluation: the operands are “passed” unevaluated. Internally, the operator evaluates the first operand in any case, the second only when needed. In a language like Algol 68, which allows arbitrary control flow constructs to be used inside expressions, conditional evaluation can be specified explicitly with if… then … else; see Exercise 6.13.

当用于确定选择或迭代构造中的控制流时,短路布尔表达式实际上不必计算布尔值;它们只需确保控制在任何给定情况下都采用正确的路径。我们将在第6.4.1 节中更详细地介绍短路表达式的代码生成。

When used to determine the flow of control in a selection or iteration construct, short-circuit Boolean expressions do not really have to calculate a Boolean value; they simply have to ensure that control takes the proper path in any given situation. We will look more closely at the generation of code for short-circuit expressions in Section 6.4.1.

06-01-9780124104099检查你的理解

Check Your Understanding

12. 既然能够为变量分配值,为什么指定初始值很有用?

12. Given the ability to assign a value into a variable, why is it useful to be able to specify an initial value?

13. 什么是聚合体?它们有什么用处?

13. What are aggregates? Why are they useful?

14.解释 Java 和 C# 中的明确赋值概念。

14. Explain the notion of definite assignment in Java and C#.

15. 为什么在运行时捕获所有未初始化变量的使用通常很昂贵?

15. Why is it generally expensive to catch all uses of uninitialized variables at run time?

16. 为什么不可能在编译时捕获所有未初始化变量的使用?

16. Why is it impossible to catch all uses of uninitialized variables at compile time?

17. 为什么大多数语言没有指定运算符或函数参数的求值顺序?

17. Why do most languages leave unspecified the order in which the arguments of an operator or function are evaluated?

18. 什么是短路布尔运算? 它有什么用处?

18. What is short-circuit Boolean evaluation? Why is it useful?

6.2 结构化和非结构化流程

6.2 Structured and Unstructured Flow

例 6.39

Example 6.39

Fortran 中使用goto进行控制流

Control flow with gotos in Fortran

汇编语言中的控制流是通过条件和无条件跳转(分支)实现的。Fortran 的早期版本模仿了低级方法,对大多数非过程控制流严重依赖goto语句:

Control flow in assembly languages is achieved by means of conditional and unconditional jumps (branches). Early versions of Fortran mimicked the low-level approach by relying heavily on goto statements for most nonprocedural control flow:

 if (A .lt. B) goto 10 !“.lt.”表示“<”

 if (A .lt. B) goto 10   ! “.lt.” means “<”

 

 

10

10

底行的 10 是语句标签。Goto语句在其他早期命令语言中也占有重要地位。■

The 10 on the bottom line is a statement label. Goto statements also featured prominently in other early imperative languages. ■

从 20 世纪 60 年代末开始,主要是为了回应 Edsger Dijkstra [ Dij68b ] 的一篇文章,6 位语言设计者就 goto 的优缺点展开了激烈的辩论。可以说,反对者获胜了。Ada 和 C# 仅在有限的上下文中允许 goto。Modula(1、2 和 3)、Clu、Eiffel、Java 和大多数脚本语言根本不允许使用 goto。Fortran 90 和 C++ 允许使用 goto 主要是为了与它们的前身语言兼容。(Java 将标记 goto 保留为关键字,以便当程序员错误地使用 C++ goto 时,Java 编译器更容易生成良好的错误消息。)

Beginning in the late 1960s, largely in response to an article by Edsger Dijkstra [Dij68b],6 language designers hotly debated the merits and evils of gotos. It seems fair to say the detractors won. Ada and C# allow gotos only in limited contexts. Modula (1, 2, and 3), Clu, Eiffel, Java, and most of the scripting languages do not allow them at all. Fortran 90 and C++ allow them primarily for compatibility with their predecessor languages. (Java reserves the token goto as a keyword, to make it easier for a Java compiler to produce good error messages when a programmer uses a C++ goto by mistake.)

放弃 goto 是软件工程领域一场更大的“革命”的一部分,即结构化编程。结构化编程是 20 世纪 70 年代的“热门趋势”,就像面向对象编程是 20 世纪 90 年代的趋势一样。结构化编程强调自上而下的设计(即逐步细化)、代码模块化、结构化类型(记录、集合、指针、多维数组)、描述性变量和常量名称以及广泛的注释约定。结构化编程的开发人员能够证明,在子程序中,几乎任何设计良好的命令式算法都可以仅通过排序、选择和迭代来优雅地表达。结构化语言不使用标签,而是依靠词汇嵌套结构的边界作为分支控制的目标。

The abandonment of gotos was part of a larger “revolution” in software engineering known as structured programming. Structured programming was the “hot trend” of the 1970s, in much the same way that object-oriented programming was the trend of the 1990s. Structured programming emphasizes top-down design (i.e., progressive refinement), modularization of code, structured types (records, sets, pointers, multidimensional arrays), descriptive variable and constant names, and extensive commenting conventions. The developers of structured programming were able to demonstrate that within a subroutine, almost any well-designed imperative algorithm can be elegantly expressed with only sequencing, selection, and iteration. Instead of labels, structured languages rely on the boundaries of lexically nested constructs as the targets of branching control.

Algol 60 开创了许多现代程序员熟悉的结构化控制流构造。其中包括if…then…else构造以及枚举(for)和逻辑(while)控制循环。现代 case (switch)语句由 Wirth 和 Hoare 在 Algol W [ WH66 ]中引入,分别作为 Fortran 和 Algol 60 中非结构化的计算 goto 和 switch 构造的替代方案。(C 的 switch 语句与 Algol W case语句的相似性比与 Algol 60 switch的相似性更高。)

Many of the structured control-flow constructs familiar to modern programmers were pioneered by Algol 60. These include the if… then … else construct and both enumeration (for) and logically (while) controlled loops. The modern case (switch) statement was introduced by Wirth and Hoare in Algol W [WH66] as an alternative to the more unstructured computed goto and switch constructs of Fortran and Algol 60, respectively. (The switch statement of C bears a closer resemblance to the Algol W case statement than to the Algol 60 switch.)

6.2.1 goto的结构化替代方案

6.2.1 Structured Alternatives to goto

一旦定义了主要的结构化构造,围绕goto的大部分争议就围绕着少数特殊情况,每种情况最终都以结构化的方式得到解决。以前 goto可能用于跳转到当前子程序的末尾,而现在大多数现代语言都提供了显式的return语句。以前goto可能用于退出循环中间,而现在大多数现代语言都提供了breakexit语句来达到此目的。(某些语言还提供了只跳过当前迭代剩余部分的语句:C 中的continue ; Fortran 90 中的cycle ;Perl 中的next。)更重要的是,有几种语言允许程序在单个操作中从嵌套的子程序调用链中返回,许多语言都提供了一种引发异常并传播到周围环境的方法。这两种功能可能都曾​​尝试使用(非本地)goto来实现。

Once the principal structured constructs had been defined, most of the controversy surrounding gotos revolved around a small number of special cases, each of which was eventually addressed in structured ways. Where once a goto might have been used to jump to the end of the current subroutine, most modern languages provide an explicit return statement. Where once a goto might have been used to escape from the middle of a loop, most modern languages provide a break or exit statement for this purpose. (Some languages also provide a statement that will skip the remainder of the current iteration only: continue in C; cycle in Fortran 90; next in Perl.) More significantly, several languages allow a program to return from a nested chain of subroutine calls in a single operation, and many provide a way to raise an exception that propagates out to some surrounding context. Both of these capabilities might once have been attempted with (nonlocal) gotos.

多级回报

Multilevel Returns

例 6.40

Example 6.40

退出嵌套子程序

Escaping a nested subroutine

返回和(局部)goto允许控制从当前子程序返回。有时从周围程序返回是有意义的。例如,假设我们正在文件集合中搜索与某些所需模式匹配的项目。搜索程序可能会调用几个嵌套程序,或多次调用单个程序,每个要搜索的位置调用一次。在这种情况下,某些历史语言(包括 Algol 60、PL/I 和 Pascal)允许 goto分支到当前子程序之外的词汇可见标签:

Returns and (local) gotos allow control to return from the current subroutine. On occasion it may make sense to return from a surrounding routine. Imagine, for example, that we are searching for an item matching some desired pattern within a collection of files. The search routine might invoke several nested routines, or a single routine multiple times, once for each place in which to search. In such a situation certain historic languages, including Algol 60, PL/I, and Pascal, permitted a goto to branch to a lexically visible label outside the current subroutine:

函数搜索(键:字符串):字符串;

function search(key : string) : string;

var rtn:字符串;

var rtn : string;

 过程search_file(fname:string);

 procedure search_file(fname : string);

 

 

 开始

 begin

  

  

  for … (* 遍历各行 *)

  for … (* iterate over lines *)

   

   

   如果找到(键,行)则开始

   if found(key, line) then begin

    rtn:=线;

    rtn := line;

    转到100;

    goto 100;

   结尾;

   end;

   

   

 结尾;

 end;

开始 (* 搜索 *)

begin (* search *)

 

 

 for … (* 迭代文件 *)

 for … (* iterate over files *)

  

  

  搜索文件(文件名称);

  search_file(fname);

  

  

100:返回rtn;

100:  return rtn;

 结束;

 end;

如果发生非本地goto,语言实现必须保证修复子例程调用信息的运行时堆栈。此修复操作称为展开。它不仅要求实现释放我们已退出的任何子例程的堆栈框架,还要求它执行任何簿记操作,例如恢复寄存器内容,这些操作将在从这些例程返回时执行。

In the event of a nonlocal goto, the language implementation must guarantee to repair the run-time stack of subroutine call information. This repair operation is known as unwinding. It requires not only that the implementation deallocate the stack frames of any subroutines from which we have escaped, but also that it perform any bookkeeping operations, such as restoration of register contents, that would have been performed when returning from those routines.

作为非局部goto的更结构化的替代方案,Common Lisp 提供了一个return-from语句,该语句命名要返回的词汇上周围的函数或块,并且还提供返回值(消除了示例 6.40中对人工rtn变量的需要)。

As a more structured alternative to the nonlocal goto, Common Lisp provides a return-from statement that names the lexically surrounding function or block from which to return, and also supplies a return value (eliminating the need for the artificial rtn variable in Example 6.40).

例 6.41

Example 6.41

结构化非本地转移

Structured nonlocal transfers

但是,如果search_file不嵌套在search 中会怎么样?例如,我们可能希望从以不同顺序搜索文件的例程中调用它。Algol 60、Algol 68 和 PL/I 允许将标签作为参数传递,因此动态嵌套的子例程可以执行goto到调用者定义的位置。Common Lisp 再次提供了一种更结构化的替代方案,在 Ruby 中也可用。在这两种语言中,表达式都可以用catch块包围,其值可以由执行匹配throw的任何动态嵌套例程提供。在 Ruby 中,我们可以写

But what if search_file were not nested inside of search? We might, for example, wish to call it from routines that search files in different orders. Algol 60, Algol 68, and PL/I allowed labels to be passed as parameters, so a dynamically nested subroutine could perform a goto to a caller-defined location. Common Lisp again provides a more structured alternative, also available in Ruby. In either language an expression can be surrounded with a catch block, whose value can be provided by any dynamically nested routine that executes a matching throw. In Ruby we might write

def searchFile(fname,模式)

def searchFile(fname, pattern)

 文件 = 文件.打开(文件名称)

 file = File.open(fname)

 文件.每个{|行|

 file.each {|line|

  抛出:found,line if line =~ /#{pattern}/

  throw :found, line if line =~ /#{pattern}/

 }

 }

结尾

end

匹配 = 捕获:找到

match = catch :found do

 searchFile(“f1”, 键)

 searchFile(“f1”, key)

 searchFile(“f2”, 键)

 searchFile(“f2”, key)

 searchFile(“f3”, 键)

 searchFile(“f3”, key)

 “not found\n” # catch 的默认值,

 “not found\n” #   default value for catch,

如果控制到达这里就结束#

end   # if control gets this far

打印匹配

print match

此处,throw 表达式指定了一个tag,该 tag 必须出现在匹配的catch中,同时还指定了一个值(行),该值将作为catch的值返回。(throw附带的if子句执行正则表达式模式匹配,在行中查找模式。我们将在14.4.2 节中更详细地讨论模式匹配。)■

Here the throw expression specifies a tag, which must appear in a matching catch, together with a value (line) to be returned as the value of the catch. (The if clause attached to the throw performs a regular-expression pattern match, looking for pattern within line. We will consider pattern matching in more detail in Section 14.4.2.) ■

错误和其他异常

Errors and Other Exceptions

多级返回的概念假设被调用者知道调用者期望什么,并能返回适当的值。在相关的、可能更常见的情况下,深度嵌套的块或子程序可能会发现它无法继续执行其通常的功能,而且缺少以任何优雅方式恢复所需的上下文信息。Eiffel 通过以下说法正式化了这一概念:每个软件组件都有一个契约——它所执行的功能的规范。无法履行其契约的组件被称为失败。它不能以正常方式返回,而是必须安排控制权“退出”到程序能够恢复的某个上下文。需要程序“退出”的条件通常称为异常。我们在C-2.3.5 节中提到了一个例子,其中我们考虑了从递归下降解析器中的语法错误进行短语级恢复。

The notion of a multilevel return assumes that the callee knows what the caller expects, and can return an appropriate value. In a related and arguably more common situation, a deeply nested block or subroutine may discover that it is unable to proceed with its usual function, and moreover lacks the contextual information it would need to recover in any graceful way. Eiffel formalizes this notion by saying that every software component has a contract—a specification of the function it performs. A component that is unable to fulfill its contract is said to fail. Rather than return in the normal way, it must arrange for control to “back out” to some context in which the program is able to recover. Conditions that require a program to “back out” are usually called exceptions. We mentioned an example in Section C-2.3.5, where we considered phrase-level recovery from syntax errors in a recursive descent parser.

例 6.42

Example 6.42

使用状态代码进行错误检查

Error checking with status codes

处理异常最直接但通常最不令人满意的方法是在子程序中使用辅助布尔变量(if still_ok then …)并从调用中返回状态代码:

The most straightforward but generally least satisfactory way to cope with exceptions is to use auxiliary Boolean variables within a subroutine (if still_ok then … ) and to return status codes from calls:

状态:= my_proc(args);

status := my_proc(args);

如果状态 = ok 那么 …

if status = ok then …

可以使用非本地goto或多级返回来消除辅助布尔值,但是我们返回到的调用者仍然必须明确检查状态代码。作为一种结构化的替代方案,许多现代语言提供了一种异常处理机制,以便从异常中进行方便的非本地恢复。我们将在第 9.4 节中更详细地讨论异常处理。通常,程序员会将一个称为处理程序的代码块附加到可能出现异常的任何计算中。处理程序的工作是采取从异常中恢复所需的任何补救措施。如果受保护的计算以正常方式完成,则跳过处理程序的执行。

The auxiliary Booleans can be eliminated by using a nonlocal goto or multilevel return, but the caller to which we return must still inspect status codes explicitly. As a structured alternative, many modern languages provide an exception-handling mechanism for convenient, nonlocal recovery from exceptions. We will discuss exception handling in more detail in Section 9.4. Typically the programmer appends a block of code called a handler to any computation in which an exception may arise. The job of the handler is to take whatever remedial action is required to recover from the exception. If the protected computation completes in the normal fashion, execution of the handler is skipped.

多级返回和结构化异常具有很强的相似性。两者都涉及从某个内部嵌套上下文返回到外部上下文的控制转移,并在途中展开堆栈。区别在于计算发生的位置。在多级返回中,内部上下文拥有所需的所有信息。它完成计算,在适当的情况下生成返回值,并以不需要后处理的方式转移到外部上下文。相比之下,在异常情况下,内部上下文无法完成其工作 - 它无法履行其合同。它执行“异常”返回,触发处理程序的执行。

Multilevel returns and structured exceptions have strong similarities. Both involve a control transfer from some inner, nested context back to an outer context, unwinding the stack on the way. The distinction lies in where the computing occurs. In a multilevel return the inner context has all the information it needs. It completes its computation, generating a return value if appropriate, and transfers to the outer context in a way that requires no post-processing. At an exception, by contrast, the inner context cannot complete its work—it cannot fulfill its contract. It performs an “abnormal” return, triggering execution of the handler.

Common Lisp 和 Ruby 都提供了多级返回和异常的机制,但这种双重支持相对较少见。大多数语言仅支持异常;程序员通过编写简单的处理程序来实现多级返回。不幸的是,术语过多,Common Lisp 和 Ruby 用于多级返回的名称catchthrow在其他几种语言中用于异常。

Common Lisp and Ruby provide mechanisms for both multilevel returns and exceptions, but this dual support is relatively rare. Most languages support only exceptions; programmers implement multilevel returns by writing a trivial handler. In an unfortunate overloading of terminology, the names catch and throw, which Common Lisp and Ruby use for multilevel returns, are used for exceptions in several other languages.

6.2.2 延续

6.2.2 Continuations

通过定义所谓的延续,可以概括非局部goto (展开堆栈)的概念从低级角度来说,延续由代码地址、跳转到该地址时应建立(或恢复)的引用环境以及对另一个延续的引用组成,该引用表示在后续子例程返回时应执行的操作。(返回延续的链构成了运行时堆栈的回溯。)从高级角度来说,延续是一种抽象,它捕获了执行可能继续的上下文。延续是指向性语义的基础。它们还作为一等值出现在几种编程语言(尤其是 Scheme 和 Ruby)中,允许程序员定义新的控制流构造。

The notion of nonlocal gotos that unwind the stack can be generalized by defining what are known as continuations. In low-level terms, a continuation consists of a code address, a referencing environment that should be established (or restored) when jumping to that address, and a reference to another continuation that represents what to do in the event of a subsequent subroutine return. (The chain of return continuations constitutes a backtrace of the run-time stack.) In higher-level terms, a continuation is an abstraction that captures a context in which execution might continue. Continuations are fundamental to denotational semantics. They also appear as first-class values in several programming languages (notably Scheme and Ruby), allowing the programmer to define new control-flow constructs.

Scheme 中的延续支持采用名为call-with-current-continuation的函数形式,通常缩写为call/cc。此函数接受单个参数f,而 f 本身是一个参数的函数。Call /cc调用f ,将延续c作为参数传递,该延续捕获当前程序计数器、引用环境和堆栈回溯。延续以闭包形式实现,与用于表示作为参数传递的子例程的闭包没有区别。在未来的任何时候,f都可以调用c,向其传递一个值v。该调用将“返回” vc的捕获上下文中,就好像它是由对call/cc的原始调用返回的一样。

Continuation support in Scheme takes the form of a function named call-with-current-continuation, often abbreviated call/cc. This function takes a single argument f, which is itself a function of one argument. Call/cc calls f, passing as argument a continuation c that captures the current program counter, referencing environment, and stack backtrace. The continuation is implemented as a closure, indistinguishable from the closures used to represent subroutines passed as parameters. At any point in the future, f can call c, passing it a value, v. The call will “return” v into c's captured context, as if it had been returned by the original call to call/cc.

例 6.43

Example 6.43

一个简单的 Ruby 延续

A simple Ruby continuation

Ruby 支持类似:

Ruby support is similar:

定义 foo(i,c)

def foo(i, c)

 printf “开始 %d; “, 我

 printf “start %d; “, i

 如果 i < 3 则 foo(i+1,c) 否则 c.call(i) 结束

 if i < 3 then foo(i+1, c) else c.call(i) end

 printf “结束%d;”,我

 printf “end %d; “, i

结尾

end

v = callcc { |d| foo(1,d) }

v = callcc { |d| foo(1, d) }

printf “得到 %d\n”, v

printf “got %d\n”, v

这里callcc的参数是一个——大致是一个 lambda 表达式。块的参数是延续c,它被其主体传递,连同1 号,传给子程序foo。然后子程序在执行c.call(i)之前递归调用自身两次。最后,call方法跳转到c捕获的上下文,使i(即3 )看起来像是由callcc返回的。最终程序输出为start 1 ; start 2 ; start 3 ; got 3。■

Here the parameter to callcc is a block—roughly, a lambda expression. The block's parameter is the contin uation c, which its body passes, together with the number 1, to subroutine foo. The subroutine then calls itself twice recursively before executing c.call(i). Finally, the call method jumps into the context captured by c, making i (that is, 3) appear to have been returned by callcc. The final program output is start 1; start 2; start 3; got 3. ■

设计与实现

Design & Implementation

6.4 清理延续

6.4 Cleaning up continuations

Scheme 和 Ruby 中延续的实现非常简单。由于局部变量在这两种语言中都具有无限范围,因此通常必须在堆上分配激活记录。因此,在跳过延续时,在当前上下文中显式释放框架既不是必需的,也不合适:如果这些框架不再可访问,它们最终将被标准垃圾收集器回收(有关这方面的更多信息,请参阅第 8.5.3 节)。

The implementation of continuations in Scheme and Ruby is surprisingly straightforward. Because local variables have unlimited extent in both languages, activation records must in general be allocated on the heap. As a result, explicit deallocation of frames in the current context is neither required nor appropriate when jumping through a continuation: if those frames are no longer accessible, they will eventually be reclaimed by the standard garbage collector (more on this in Section 8.5.3).

例 6.44

Example 6.44

延续重用和无限范围

Continuation reuse and unlimited extent

在这个简单的例子中,跳转到延续的行为与异常非常相似,会从一系列嵌套调用中弹出。但延续可以做更多的事情。与其他闭包一样,它们可以保存在变量中,从子例程返回,或重复调用,即使在控制权从创建它们的上下文中返回之后也是如此(这意味着它们需要无限的范围;参见第 3.6 节)。考虑以下更微妙的例子:

In this simple example, the jump into the continuation behaved much as an exception would, popping out of a series of nested calls. But continuations can do much more. Like other closures, they can be saved in variables, returned from subroutines, or called repeatedly, even after control has returned out of the context in which they were created (this means that they require unlimited extent; see Section 3.6). Consider the following more subtle example:

绝对在这里

def here

 返回 callcc { |a| 返回 a }

 return callcc { |a| return a }

结尾

end

定义条形码(i)

def bar(i)

 printf “开始 %d; “, 我

 printf “start %d; “, i

 b = 如果 i < 3 则 bar(i+1) 否则此处结束

 b = if i < 3 then bar(i+1) else here end

 printf “结束%d;”,我

 printf “end %d; “, i

 返回 b

 return b

结尾

end

n = 3

n = 3

c = 条形图(1)

c = bar(1)

n -= 1

n -= 1

放 # 打印换行符

puts # print newline

如果 n > 0 则 c.call(c) 结束

if n > 0 then c.call(c) end

放“完成”

puts “done”

此代码对bar执行了三次嵌套调用,返回了在最内层调用中间由函数创建的延续。使用该延续,我们可以跳回到 bar 的嵌套调用中事实上我们可以重复执行此操作。请注意,虽然c捕获的引用环境每次都保持不变,但n的值可能会发生变化。最终的程序输出是

This code performs three nested calls to bar, returning a continuation created by function here in the middle of the innermost call. Using that continuation, we can jump back into the nested calls of bar—in fact, we can do so repeatedly. Note that while c's captured referencing environment remains the same each time, the value of n can change. The final program output is

开始 1;开始 2;开始 3;结束 3;结束 2;结束 1;

start 1; start 2; start 3; end 3; end 2; end 1;

结束 3;结束 2;结束 1;

end 3; end 2; end 1;

结束 3;结束 2;结束 1;

end 3; end 2; end 1;

完成

done

Call/cc足以构建各种控制抽象,包括goto、循环中退出、多级返回、异常、迭代器(第 6.5.3 节)、按名称调用参数(第 9.3.1 节)和协同程序(第 9.5 节)。它甚至包含了从子程序返回的概念,尽管在实践中它很少取代它。如果以规范的方式使用,延续性会使语言具有惊人的可扩展性。同时,它们允许不规范的程序员构建完全难以捉摸的程序。

Call/cc suffices to build a wide variety of control abstractions, including gotos, midloop exits, multilevel returns, exceptions, iterators (Section 6.5.3), call-by-name parameters (Section 9.3.1), and coroutines (Section 9.5). It even subsumes the notion of returning from a subroutine, though it seldom replaces it in practice. Used in a disciplined way, continuations make a language surprisingly extensible. At the same time, they allow the undisciplined programmer to construct completely inscrutable programs.

6.3 测序

6.3 Sequencing

与赋值一样,排序是命令式编程的核心。它是控制副作用(例如赋值)发生顺序的主要手段:当程序文本中一条语句跟在另一条语句后面时,第一条语句先于第二条语句执行。在大多数命令式语言中,语句列表可以用begin…end{ … }分隔符括起来,然后在任何需要单个语句的上下文中使用。这种分隔列表通常称为复合语句。复合语句(可选地以一组声明开头)有时称为块。

Like assignment, sequencing is central to imperative programming. It is the principal means of controlling the order in which side effects (e.g., assignments) occur: when one statement follows another in the program text, the first statement executes before the second. In most imperative languages, lists of statements can be enclosed with begin… end or { … } delimiters and then used in any context in which a single statement is expected. Such a delimited list is usually called a compound statement. A compound statement optionally preceded by a set of declarations is sometimes called a block.

在 Algol 68 等语言中,语句和表达式之间的区别被模糊或消除,语句(表达式)列表的值是其最后一个元素的值。在 Common Lisp 中,程序员可以选择返回第一个元素、第二个元素或最后一个元素的值。当然,除非不参与返回值的子表达式有副作用,否则排序是无用的操作。Lisp 中的各种排序结构仅用于不符合纯函数式编程模型的程序片段。

In languages like Algol 68, which blur or eliminate the distinction between statements and expressions, the value of a statement (expression) list is the value of its final element. In Common Lisp, the programmer can choose to return the value of the first element, the second, or the last. Of course, sequencing is a useless operation unless the subexpressions that do not play a part in the return value have side effects. The various sequencing constructs in Lisp are used only in program fragments that do not conform to a purely functional programming model.

即使在命令式语言中,某些副作用的价值也存在争议。例如,在欧几里得和图灵中,函数(即返回值,因此可以出现在表达式中的子程序)不允许有副作用。除其他外,副作用自由确保欧几里得或图灵函数(就像数学中的对应函数一样)始终是幂等的:如果使用同一组参数重复调用,它将始终返回相同的值,并且连续调用的次数(第一次之后)不会影响后续执行的结果。此外,函数的副作用自由意味着子表达式的值永远不会取决于该子表达式是在调用其他子表达式中的函数之前还是之后求值。这些属性使程序员或定理证明系统更容易推断程序行为。它们还简化了代码改进,例如通过允许安全地重新排列表达式。

Even in imperative languages, there is debate as to the value of certain kinds of side effects. In Euclid and Turing, for example, functions (i.e., subroutines that return values, and that therefore can appear within expressions) are not permitted to have side effects. Among other things, side-effect freedom ensures that a Euclid or Turing function, like its counterpart in mathematics, is always idempotent: if called repeatedly with the same set of arguments, it will always return the same value, and the number of consecutive calls (after the first) will not affect the results of subsequent execution. In addition, side-effect freedom for functions means that the value of a subexpression will never depend on whether that subexpression is evaluated before or after calling a function in some other subexpression. These properties make it easier for a programmer or theorem-proving system to reason about program behavior. They also simplify code improvement, for example by permitting the safe rearrangement of expressions.

例 6.45

Example 6.45

随机数生成器的副作用

Side effects in a random number generator

不幸的是,在某些情况下,函数中的副作用是十分必要的。我们在图 3.3中的label_name函数中看到了一个例子。另一个例子出现在伪随机数生成器的典型接口中:

Unfortunately, there are some situations in which side effects in functions are highly desirable. We saw one example in the label_name function of Figure 3.3. Another arises in the typical interface to a pseudorandom number generator:

程序 srand(种子:整数)

procedure srand(seed : integer)

  –– 初始化内部表。

  –– Initialize internal tables.

  –– 伪随机数生成器将返回不同的

  –– The pseudorandom generator will return a different

  –– 每个不同种子值的序列值。

  –– sequence of values for each different value of seed.

函数 rand() :整数

function rand() : integer

  –– 没有参数;返回一个新的“随机”数。

  –– No arguments; returns a new “random” number.

显然 rand 需要有副作用,这样每次调用它时都会返回不同的值。我们总是可以将其重铸为带有引用参数的过程:

Obviously rand needs to have a side effect, so that it will return a different value each time it is called. One could always recast it as a procedure with a reference parameter:

过程 rand(参考 n:整数)

procedure rand(ref n : integer)

但大多数程序员会发现这不太有吸引力。Ada 达成了妥协:它允许函数以改变静态或全局变量的形式产生副作用,但不允许函数修改其参数。■

but most programmers would find this less appealing. Ada strikes a compromise: it allows side effects in functions in the form of changes to static or global variables, but does not allow a function to modify its parameters. ■

6.4 选择

6.4 Selection

例 6.46

Example 6.46

Algol 60 中的选择

Selection in Algol 60

大多数命令式语言中的选择语句都采用了Algol 60 中引入的if ... then ... else符号的某种变体:

Selection statements in most imperative languages employ some variant of the if… then … else notation introduced in Algol 60:

if条件then语句

if condition then statement

else if条件then语句

else if condition then statement

else if条件then语句

else if condition then statement

else语句

else statement

正如我们在第 2.3.2 节中看到的,不同语言在语法细节上有所不同。在 Algol 60 和 Pascal 中,then 子句和else子句都被定义为包含单个语句(当然可以是begin...end复合语句)。为了避免语法歧义,Algol 60 要求 then 后的语句以if以外的其他内容开头(begin也可以)。Pascal 消除了这一限制,转而采用“消歧义规则”,将else与最接近的未匹配的 then 相关联。Algol 68、Fortran 77 和更多现代语言通过允许语句列表跟在thenelse 之后来避免歧义,并在构造末尾使用终止关键字。

As we saw in Section 2.3.2, languages differ in the details of the syntax. In Algol 60 and Pascal both the then clause and the else clause were defined to contain a single statement (this could of course be a begin… end compound statement). To avoid grammatical ambiguity, Algol 60 required that the statement after the then begin with something other than if (begin is fine). Pascal eliminated this restriction in favor of a “disambiguating rule” that associated an else with the closest unmatched then. Algol 68, Fortran 77, and more modern languages avoid the ambiguity by allowing a statement list to follow either then or else, with a terminating keyword at the end of the construct.

例 6.47

Example 6.47

elsif/elif

elsif/elif

为了防止终止符堆积在嵌套 if 语句的末尾,大多数带有终止符的语言都提供了特殊的elsifelif关键字。在 Ruby 中,可以这样写

To keep terminators from piling up at the end of nested if statements, most languages with terminators provide a special elsif or elif keyword. In Ruby, one writes

如果 a == b 那么

if a == b then

 

 

elsif a == c 那么

elsif a == c then

 

 

elsif a == d 那么

elsif a == d then

 

 

别的

else

 

 

结束

end

例 6.48

Example 6.48

Lisp 中的cond

cond in Lisp

在 Lisp 中,等效结构是

In Lisp, the equivalent construct is

(条件

(cond

 ((= AB)

 ((= A B)

  (…))

  (…))

 ((= 交流电)

 ((= A C)

  (…))

  (…))

 ((= 公元)

 ((= A D)

  (…))

  (…))

 (T

 (T

(…)))

(…)))

这里 cond 以一系列对作为参数。每对中的第一个元素是条件;第二个元素是表达式,如果条件计算结果为T(在大多数 Lisp 方言中, T表示“真”),则该表达式将作为整体构造的值返回。■

Here cond takes as arguments a sequence of pairs. In each pair the first element is a condition; the second is an expression to be returned as the value of the overall construct if the condition evaluates to T (T means “true” in most Lisp dialects). ■

6.4.1 短路情况

6.4.1 Short-Circuited Conditions

虽然if…then…else语句中的条件是布尔表达式,但通常不需要求值该表达式以将布尔值传入寄存器。大多数机器都提供用于捕获简单比较的条件分支指令。换句话说,选择语句中布尔表达式的目的不是计算要存储的值,而是使控制分支到各个位置。这一观察使我们能够为适合6.1.5 节的短路求值的表达式生成特别高效的代码(称为跳转代码) 。跳转代码不仅适用于诸如if…then…else 之类的选择语句,也适用于逻辑控制循环;我们将在6.5.5 节中讨论后者。

While the condition in an if… then … else statement is a Boolean expression, there is usually no need for evaluation of that expression to result in a Boolean value in a register. Most machines provide conditional branch instructions that capture simple comparisons. Put another way, the purpose of the Boolean expression in a selection statement is not to compute a value to be stored, but to cause control to branch to various locations. This observation allows us to generate particularly efficient code (called jump code) for expressions that are amenable to the short-circuit evaluation of Section 6.1.5. Jump code is applicable not only to selection statements such as if… then … else, but to logically controlled loops as well; we will consider the latter in Section 6.5.5.

在通常的代码生成过程中,表达式子树根的合成属性会获取一个寄存器的名称,表达式的值将在运行时计算到该寄存器中。然后,周围的上下文在生成使用该表达式的代码时使用此寄存器名称。在跳转代码中,根的继承属性会分别告知它在表达式为真或为假时控制应该分支到的地址。

In the usual process of code generation, a synthesized attribute of the root of an expression subtree acquires the name of a register into which the value of the expression will be computed at run time. The surrounding context then uses this register name when generating code that uses the expression. In jump code, inherited attributes of the root inform it of the addresses to which control should branch if the expression is true or false, respectively.

例 6.49

Example 6.49

布尔条件的代码生成

Code generation for a Boolean condition

例如,假设我们正在为以下源生成代码:

Suppose, for example, that we are generating code for the following source:

如果 ((A > B) 且 (C > D)) 或 (E ≠ F),则

if ((A > B) and (C > D)) or (E ≠ F) then

          then_clause

          then_clause

别的

else

          else_子句

          else_clause

在没有短路求值的语言中,输出代码将如下所示:

In a language without short-circuit evaluation, the output code would look something like this:

 r1 := A –– 负载

 r1 := A   –– load

 r2:=B

 r2 := B

 r1 := r1 > r2

 r1 := r1 > r2

 r2:=C

 r2 := C

 r3:=D

 r3 := D

 r2 := r2 > r3

 r2 := r2 > r3

 r1:= r1&r2

 r1 := r1 & r2

 r2:=E

 r2 := E

 r3:=F

 r3 := F

 r2 := r2 ≠ r3

 r2 := r2 ≠ r3

 r1 := r1 | r2

 r1 := r1 | r2

 如果 r1 = 0 转到 L2

 if r1 = 0 goto L2

L1:then_clause   ––(标签实际上未使用)

L1: then_clause   –– (label not actually used)

 转到 L3

 goto L3

L2:else_子句

L2: else_clause

三级:

L3:

((A > B) 和 (C > D)) 或 (E ≠ F)的子树的根将r1命名为包含表达式值的寄存器。■

The root of the subtree for ((A > B) and (C > D)) or (E ≠ F) would name r1 as the register containing the expression value. ■

例 6.50

Example 6.50

短路代码生成

Code generation for short-circuiting

相比之下,在跳转代码中,条件根的继承属性将指示如果条件为真,则控制应“落入” L1 ,如果条件为假,则分支到L2。输出代码将如下所示:

In jump code, by contrast, the inherited attributes of the condition's root would indicate that control should “fall through” to L1 if the condition is true, or branch to L2 if the condition is false. Output code would then look something like this:

 r1:= A

 r1 := A

 r2:=B

 r2 := B

 如果 r1 <= r2 则转到 L4

 if r1 <= r2 goto L4

 r1:=C

 r1 := C

 r2:=D

 r2 := D

 如果 r1 > r2 则转到 L1

 if r1 > r2 goto L1

L4: r1:= E

L4: r1 := E

 r2:= F

 r2 := F

 如果 r1 = r2 转到 L2

 if r1 = r2 goto L2

L1:then_clause

L1: then_clause

 转到 L3

 goto L3

L2:else_子句

L2: else_clause

三级:

L3:

这里,布尔条件的值从未明确放入寄存器中。相反,它隐含在控制流中。此外,对于A、B、C、DE的大多数值,通过跳转代码的执行路径比计算每个子表达式的值的直线代码更短,因此更快(假设分支预测良好)。■

Here the value of the Boolean condition is never explicitly placed into a register. Rather it is implicit in the flow of control. Moreover for most values of A, B, C, D, and E, the execution path through the jump code is shorter and therefore faster (assuming good branch prediction) than the straight-line code that calculates the value of every subexpression. ■

设计与实现

Design & Implementation

6.5 短路评估

6.5 Short-circuit evaluation

短路求值是编程语言设计中令人欣喜的案例之一,巧妙的语言特性可以产生比现有替代方案更有用的语义更快的实现。其他至少值得商榷的例子包括 case 语句、for 循环索引的本地范围(第 6.5.1 节)和 Ada 样式的参数模式(第 9.3.1 节)。

Short-circuit evaluation is one of those happy cases in programming language design where a clever language feature yields both more useful semantics and a faster implementation than existing alternatives. Other at least arguable examples include case statements, local scopes for for loop indices (Section 6.5.1), and Ada-style parameter modes (Section 9.3.1).

例 6.51

Example 6.51

布尔值的短路创建

Short-circuit creation of a Boolean value

如果明确需要短路表达式的值,当然可以生成它,同时仍然使用跳转代码以提高效率。Ada 片段

If the value of a short-circuited expression is needed explicitly, it can of course  be generated, while still using jump code for efficiency. The Ada fragment

found_it := p /= null 然后 p.key = val;

found_it := p /= null and then p.key = val;

相当于

is equivalent to

如果 p /= null 则 p.key = val

if p /= null and then p.key = val then

 找到它:= true;

 found_it := true;

别的

else

 找到它:= false;

 found_it := false;

结束如果;

end if;

可以翻译为

and can be translated as

 r1:=p

 r1 := p

 如果 r1 = 0 转到 L1

 if r1 = 0 goto L1

 r2 := r1→密钥

 r2 := r1→key

 如果 r2 ≠ val 则转到 L1

 if r2 ≠ val goto L1

 r1:= 1

 r1 := 1

 转到 L2

 goto L2

L1: r1:= 0

L1: r1 := 0

L2:找到它:=r1

L2: found_it := r1

精明的读者会注意到第一个goto L1可以用goto L2替换,因为在这种情况下r1已经包含零。编译器的代码改进阶段也会注意到这一点,并进行更改。在代码改进器中修复这类问题比首先生成更好的代码版本更容易。代码改进器无论如何都必须能够识别出于其他原因而跳转到冗余指令;没有必要在短路评估例程中构建特殊情况。■

The astute reader will notice that the first goto L1 can be replaced by goto L2, since r1 already contains a zero in this case. The code improvement phase of the compiler will notice this also, and make the change. It is easier to fix this sort of thing in the code improver than it is to generate the better version of the code in the first place. The code improver has to be able to recognize jumps to redundant instructions for other reasons anyway; there is no point in building special cases into the short-circuit evaluation routines. ■

6.4.2 Case/Switch语句

6.4.2 Case/Switch Statements

例 6.52

Example 6.52

case语句和嵌套if

case statements and nested ifs

Algol W 及其后代的case语句为嵌套if…then…else的特殊情况提供了替代语法。当每个条件将相同的表达式与不同的编译时常量进行比较时,则以下代码(此处以 Ada 编写)

The case statements of Algol W and its descendants provide alternative syntax for a special case of nested if… then … else. When each condition compares the same expression to a different compile-time constant, then the following code (written here in Ada)

i := … -- 潜在的复杂表达式

i := … -- potentially complicated expression

如果 i = 1 那么

if i = 1 then

 条款_A

 clause_A

否则,如果 i = 2 或 i = 7

elsif i = 2 or i = 7 then

 条款_B

 clause_B

否则,如果我在 3..5 中,则

elsif i in 3..5 then

 条款_C

 clause_C

否则,如果 i = 10

elsif i = 10 then

 条款_D

 clause_D

别的

else

 条款_E

 clause_E

结束如果;

end if;

可以改写为

can be rewritten as

case …——潜在的复杂表达式

case … -- potentially complicated expression

is

 当 1 =>子句_A

 when 1 => clause_A

 当 2 | 7 =>子句_B

 when 2 | 7 => clause_B

 当 3..5 =>子句_C

 when 3..5 => clause_C

 当 10 =>子句_D

 when 10 => clause_D

 当其他人 =>条款_E

 when others => clause_E

结束案件;

end case;

箭头后面省略的代码片段(clause_Aclause_B等)称为case语句的分支。箭头前面的常量列表是case语句标签。标签列表中的常量必须是不相交的,并且必须是与测试(“控制”)表达式兼容的类型。大多数语言允许此类型为任何值离散的变量:整数、字符、枚举和它们的子范围。C# 和(最新版本的)Java 也允许字符串。■

The elided code fragments (clause_A, clause_B, etc.) after the arrows are called the arms of the case statement. The lists of constants in front of the arrows are case statement labels. The constants in the label lists must be disjoint, and must be of a type compatible with the tested (“controlling”) expression. Most languages allow this type to be anything whose values are discrete: integers, characters, enumerations, and subranges of the same. C# and (recent versions of) Java allow strings as well. ■

例 6.53

Example 6.53

嵌套if的翻译

Translation of nested ifs

上述代码的case语句版本肯定比if…then…else版本简洁,但语法优雅并不是在编程语言中提供case语句的主要动机。主要动机是为了便于生成高效的目标代码。if …then…else语句最自然的翻译如下:

The case statement version of the code above is certainly less verbose than the if… then … else version, but syntactic elegance is not the principal motivation for providing a case statement in a programming language. The principal motivation is to facilitate the generation of efficient target code. The if… then … else statement is most naturally translated as follows:

 r1 := … –– 计算控制表达式

 r1 := …    –– calculate controlling expression

 如果 r1 ≠ 1 则转到 L1

 if r1 ≠ 1 goto L1

 条款_A

 clause_A

 转到 L6

 goto L6

L1:如果 r1 = 2,转到 L2

L1: if r1 = 2 goto L2

 如果 r1 ≠ 7 则转到 L3

 if r1 ≠ 7 goto L3

L2:条款_B

L2: clause_B

 转到 L6

 goto L6

L3:如果 r1 < 3 转到 L4

L3: if r1 < 3 goto L4

 如果 r1 > 5 转到 L4

 if r1 > 5 goto L4

 条款_C

 clause_C

 转到 L6

 goto L6

L4:如果 r1 ≠ 10,则转到 L5

L4: if r1 ≠ 10 goto L5

 条款D

 clause D

 转到 L6

 goto L6

L5:条款_E

L5: clause_E

L6:

L6:

例 6.54

Example 6.54

跳转表

Jump tables

case语句不是针对一系列可能值依次测试其控制表达式,而是通过一条指令计算出跳转到的地址。预期目标代码的一般形式如图6.3所示。标签L6处省略的计算可以采用多种形式。其中最常见的只是对数组进行索引,如图6.4所示。

Rather than test its controlling expression sequentially against a series of possible values, the case statement is meant to compute an address to which it jumps in a single instruction. The general form of the anticipated target code appears in Figure 6.3. The elided calculation at label L6 can take any of several forms. The most common of these simply indexes into an array, as shown in Figure 6.4.

编号:F06-03-9780124104099
图 6.3 为五臂 case 语句生成的目标代码的一般形式
编号:F06-04-9780124104099
图 6.4 在 case 语句中跳转表以控制分支。此代码替换了图 6.3的最后三行。

该图中标签T处的“代码”实际上是一个地址数组,称为跳转表。它包含case语句标签中最低值和最高值(含)之间的每个整数的一个条目。L6的代码检查以确保控制表达式在数组的范围内(如果不是,我们应该执行 case 语句的其他部分)。然后它从表中获取相应的条目并分支到它。■

The “code” at label T in that figure is in fact an array of addresses, known as a jump table. It contains one entry for each integer between the lowest and highest values, inclusive, found among the case statement labels. The code at L6 checks to make sure that the controlling expression is within the bounds of the array (if not, we should execute the others arm of the case statement). It then fetches the corresponding entry from the table and branches to it. ■

替代实现方案

Alternative Implementations

跳转表速度很快:无论控制表达式的值如何,它都会在恒定时间内开始执行case语句的正确分支。当整个case语句标签集密集且不包含大范围时,它也是空间高效的。但是,如果标签集不密集或包含大值范围,它可能会消耗大量空间。计算分支地址的替代方法包括顺序测试、散列和二分搜索。如果case语句标签的总数很少,则顺序测试(如if…then…else语句)是首选方法。它在O ( n ) 时间内选择一个分支,其中n是标签数。如果标签值集很大,但有许多缺失值且没有大范围,则哈希表很有吸引力。使用适当的哈希函数,它将在O (1) 时间内选择正确的分支。不幸的是,哈希表(如跳转表)需要为控制测试表达式的每个可能值设置一个单独的条目,因此不适合具有大值范围的语句。二分查找可以轻松适应范围。它可以在O (log n ) 时间内选择一个分支。

A jump table is fast: it begins executing the correct arm of the case statement in constant time, regardless of the value of the controlling expression. It is also space efficient when the overall set of case statement labels is dense and does not contain large ranges. It can consume an extraordinarily large amount of space, however, if the set of labels is nondense, or includes large value ranges. Alternative methods to compute the address to which to branch include sequential testing, hashing, and binary search. Sequential testing (as in an if… then … else statement) is the method of choice if the total number of case statement labels is small. It chooses an arm in O(n) time, where n is the number of labels. A hash table is attractive if the set of label values is large, but has many missing values and no large ranges. With an appropriate hash function it will choose the right arm in O(1) time. Unfortunately, a hash table, like a jump table, requires a separate entry for each possible value of the controlling tested expression, making it unsuitable for statements with large value ranges. Binary search can accommodate ranges easily. It chooses an arm in O(log n) time.

为了为所有可能的case语句生成良好的代码,编译器需要准备好使用各种策略。在编译期间,它可以在找到 case 语句的各个分支时为其生成代码,同时构建内部数据结构来描述标签集。一旦它看到了所有分支,它就可以决定生成哪种形式的目标代码。为了简单起见,大多数编译器只采用一些可能的实现。有些使用二分搜索代替散列。有些只生成跳转表;其他只生成跳转表加上顺序测试。如果生成的代码意外地大或慢,不太复杂的编译器的用户可能需要重构他们的case语句。

To generate good code for all possible case statements, a compiler needs to be prepared to use a variety of strategies. During compilation it can generate code for the various arms of the case statement as it finds them, while simultaneously building up an internal data structure to describe the label set. Once it has seen all the arms, it can decide which form of target code to generate. For the sake of simplicity, most compilers employ only some of the possible implementations. Some use binary search in lieu of hashing. Some generate only jump tables; others only that plus sequential testing. Users of less sophisticated compilers may need to restructure their case statements if the generated code turns out to be unexpectedly large or slow.

语法和标签语义

Syntax and Label Semantics

if…then…else语句一样, case语句的语法细节因语言而异。不同的语言使用不同的标点符号来分隔标签和分支。更重要的是,语言在是否允许标签范围、是否允许(或要求)默认(others)子句以及在运行时如何处理无法匹配任何标签的值方面存在差异。

As with if… then … else statements, the syntactic details of case statements vary from language to language. Different languages use different punctuation to delimit labels and arms. More significantly, languages differ in whether they permit label ranges, whether they permit (or require) a default (others) clause, and in how they handle a value that fails to match any label at run time.

在某些语言(例如,Modula)中,控制表达式具有未出现在标签列表中的值是一种动态语义错误。 Ada要求标签覆盖控制表达式类型的域中的所有可能值;如果类型具有非常大的值,则必须使用范围或其他子句来实现此覆盖。在某些语言中,特别是 C 和 Fortran 90,测试表达式求值为缺失值并不是错误。相反,当值缺失时,整个构造不起作用。

In some languages (e.g., Modula), it is a dynamic semantic error for the controlling expression to have a value that does not appear in the label lists. Ada requires the labels to cover all possible values in the domain of the controlling expression's type; if the type has a very large number of values, then this coverage must be accomplished using ranges or an others clause. In some languages, notably C and Fortran 90, it is not an error for the tested expression to evaluate to a missing value. Rather, the entire construct has no effect when the value is missing.

设计与实现

Design & Implementation

6.6案例陈述

6.6 Case statements

Case语句是实现驱动的语言设计最明显的例子之一。它们存在的主要原因是为了方便生成跳转表。标签列表中的范围(在 Pascal 或 C 中不允许)可能会稍微降低效率,但二分查找仍然比等效的if系列快得多。

Case statements are one of the clearest examples of language design driven by implementation. Their primary reason for existence is to facilitate the generation of jump tables. Ranges in label lists (not permitted in Pascal or C) may reduce efficiency slightly, but binary search is still dramatically faster than the equivalent series of if s.

C switch语句

The C switch Statement

C 的case (switch)语句语法(C++ 和 Java 保留了该语法)在几个方面有所不同:

C's syntax for case (switch) statements (retained by C++ and Java) is unusual in several respects:

switch (… /* 控制表达式 */) {
 情况 1:条款_A
休息;
 情况 2:
 案例7:条款_B
休息;
 案例 3:
 案例4:
 案例5:条款_C
休息;
 案例10:条款_D
休息;
 默认:条款_E
休息;
}

t0020

例 6.55

Example 6.55

C语言 switch语句中的 fall-through

Fall-through in C switch statements

这里,测试表达式的每个可能值都必须在switch内有自己的标签;不允许使用范围。实际上,不允许使用标签列表,但可以通过允许标签(例如上面的 2、3 和 4)有一个臂来实现列表的效果,该空臂只是“落入”后续标签的代码中。由于提供了落入功能,因此必须使用显式 break 语句在臂的末尾退出 switch 而不是落入下一个。在极少数情况下,落入功能很方便:

Here each possible value for the tested expression must have its own label within the switch; ranges are not allowed. In fact, lists of labels are not allowed, but the effect of lists can be achieved by allowing a label (such as 2, 3, and 4 above) to have an empty arm that simply “falls through” into the code for the subsequent label. Because of the provision for fall-through, an explicit break statement must be used to get out of the switch at the end of an arm, rather than falling through into the next. There are rare circumstances in which the ability to fall through is convenient:

字母大小写 = 小写;

letter_case = lower;

开关(c){

switch (c) {

 

 

 案例‘A’:

 case 'A' :

  字母大小写 = 大写;

  letter_case = upper;

  /* 掉下去! */

  /* FALL THROUGH! */

 案例‘a’:

 case 'a' :

  

  

  休息;

  break;

 

 

}

}

然而,大多数情况下,需要在每个分支的末尾插入一个break — 而编译器愿意默默地接受没有 break 的分支 — 是导致意外且难以诊断的错误的原因。C# 保留了熟悉的 C 语法,包括多个连续标签,但要求每个非空分支都以break、goto、continue或return结尾

Most of the time, however, the need to insert a break at the end of each arm— and the compiler's willingness to accept arms without breaks, silently—is a recipe for unexpected and difficult-to-diagnose bugs. C# retains the familiar C syntax, including multiple consecutive labels, but requires every nonempty arm to end with a break, goto, continue, or return.

06-01-9780124104099检查你的理解

Check Your Understanding

19.列出 goto的主要用途,以及每个用途的结构化替代方案。

19. List the principal uses of goto, and the structured alternatives to each.

20. 解释例外情况和多级回报之间的区别。

20. Explain the distinction between exceptions and multilevel returns.

21. 什么是延续?它们包含哪些其他语言特性?

21. What are continuations? What other language features do they subsume?

22. 为什么排序在 Lisp 中是一种相对不重要的控制流形式?

22. Why is sequencing a comparatively unimportant form of control flow in Lisp?

23. 解释为什么函数有副作用有时候是有用的。

23. Explain why it may sometimes be useful for a function to have side effects.

24. 描述短路布尔评估的跳转代码实现。

24. Describe the jump code implementation of short-circuit Boolean evaluation.

25.为什么命令式语言 除了if…then…else之外,通常还提供caseswitch语句?

25. Why do imperative languages commonly provide a case or switch statement in addition to if… then … else?

26. 描述在实现案例陈述时可能采用的三种不同的搜索策略,以及每种策略的适用情况。

26. Describe three different search strategies that might be employed in the implementation of a case statement, and the circumstances in which each would be desirable.

27. 解释如何使用 break 来终止 C switch语句的分支,以及如果意外省略 break 会出现什么行为。

27. Explain the use of break to terminate the arms of a C switch statement, and the behavior that arises if a break is accidentally omitted.

6.5 迭代

6.5 Iteration

迭代和递归是允许计算机重复执行类似操作的两种机制。如果没有其中至少一种机制,程序的运行时间(以及它可以完成的工作量和它可以使用的空间量)将是程序文本大小的线性函数。从非常实际的意义上讲,迭代和递归使计算机不仅仅适用于固定大小的任务。在本节中,我们将重点介绍迭代。递归是第6.6 节的主题。

Iteration and recursion are the two mechanisms that allow a computer to perform similar operations repeatedly. Without at least one of these mechanisms, the running time of a program (and hence the amount of work it can do and the amount of space it can use) would be a linear function of the size of the program text. In a very real sense, it is iteration and recursion that make computers useful for more than fixed-size tasks. In this section we focus on iteration. Recursion is the subject of Section 6.6.

命令式语言的程序员倾向于更多地使用迭代而不是递归(递归在函数式语言中更常见)。在大多数语言中,迭代采用循环的形式。与序列中的语句一样,循环的迭代通常是为了它们的副作用而执行的:它们对变量的修改。循环有两种主要类型,它们在用于确定迭代次数的机制上有所不同。枚举控制的循环对给定有限集中的每个值执行一次;在第一次迭代开始之前就知道迭代次数。逻辑控制的循环执行直到某个布尔条件(通常必须依赖于循环中改变的值)改变值。大多数(但不是全部)语言为这两种循环类型提供了单独的构造。

Programmers in imperative languages tend to use iteration more than they use recursion (recursion is more common in functional languages). In most languages, iteration takes the form of loops. Like the statements in a sequence, the iterations of a loop are generally executed for their side effects: their modifications of variables. Loops come in two principal varieties, which differ in the mechanisms used to determine how many times to iterate. An enumeration-controlled loop is executed once for every value in a given finite set; the number of iterations is known before the first iteration begins. A logically controlled loop is executed until some Boolean condition (which must generally depend on values altered in the loop) changes value. Most (though not all) languages provide separate constructs for these two varieties of loop.

6.5.1 枚举控制循环

6.5.1 Enumeration-Controlled Loops

例 6.56

Example 6.56

Fortran 90循环

Fortran 90 do loop

枚举控制迭代起源于Fortran I 的do 循环。几乎所有后续语言都以某种形式采用了类似的机制,但语法和语义差异很大。随着时间的推移,Fortran 自己的循环也发生了很大的变化。现代 Fortran 版本如下所示:

Enumeration-controlled iteration originated with the do loop of Fortran I. Similar mechanisms have been adopted in some form by almost every subsequent language, but syntax and semantics vary widely. Even Fortran's own loop has evolved considerably over time. The modern Fortran version looks something like this:

i = 1, 10, 2

do i = 1, 10, 2

 

 

恩多

enddo

变量i称为循环的索引。等号后面的表达式是i初始值、其界限步长。使用此处显示的值,循环体(循环头和 enddo 分隔符之间的语句)将执行五次,其中i在连续迭代中设置为 1、3、…、9。■

Variable i is called the index of the loop. The expressions that follow the equals sign are i's initial value, its bound, and the step size. With the values shown here, the body of the loop (the statements between the loop header and the enddo delimiter) will execute five times, with i set to 1,3,…, 9 in successive iterations. ■

例 6.57

Example 6.57

Modula-2 for循环

Modula-2 for loop

许多其他语言也提供类似的功能。在 Modula-2 中,有人会说

Many other languages provide similar functionality. In Modula-2 one would say

FOR i := 第一 TO 最后 BY 步骤 DO

FOR i := first TO last BY step DO

 

 

结尾

END

通过选择不同的firstlaststep值,我们可以安排对任意整数等差序列进行迭代,即i = first, first + step, …, first + 图片(last — first)/step 图片× step。■

By choosing different values of first, last, and step, we could arrange to iterate over an arbitrary arithmetic sequence of integers, namely i = first, first + step, …, first + (last — first)/step × step. ■

效仿 Clu,许多现代语言允许枚举控制循环对更通用的有限集进行迭代 — — 例如树的节点或集合的元素。我们将在第 6.5.3 节中讨论这些更通用的迭代器。目前我们重点关注算术序列。为简单起见,我们使用“ for 循环”这个名称作为通用术语,即使对于使用不同关键字的语言也是如此。

Following the lead of Clu, many modern languages allow enumeration-controlled loops to iterate over much more general finite sets—the nodes of a tree, for example, or the elements of a collection. We consider these more general iterators in Section 6.5.3. For the moment we focus on arithmetic sequences. For the sake of simplicity, we use the name “for loop“ as a general term, even for languages that use a different keyword.

for循环的代码生成

Code Generation for for Loops

例 6.58

Example 6.58

for循环的明显翻译

Obvious translation of a for loop

简单来说,示例 6.57中的循环可以翻译为

Naively, the loop of Example 6.57 can be translated as

 r1 := 第一

 r1 := first

 r2 := 步骤

 r2 := step

 r3 := 最后

 r3 := last

L1:如果 r1 > r3 则转到 L2

L1: if r1 > r3 goto L2

 … –– 循环体;使用 r1 作为 i

 …   –– loop body; use r1 for i

 r1:= r1 + r2

 r1 := r1 + r2

 转到 L1

 goto L1

L2:

L2:

例 6.59

Example 6.59

循环翻译,底部有测试

for loop translation with test at the bottom

稍微好一点但不那么直接的翻译是

A slightly better if less straightforward translation is

 r1 := 第一

 r1 := first

 r2 := 步骤

 r2 := step

 r3 := 最后

 r3 := last

 转到 L2

 goto L2

L1: … –– 循环体;使用 r1 作为 i

L1: …    –– loop body; use r1 for i

 r1:= r1 + r2

 r1 := r1 + r2

L2:如果 r1 ≤ r3,则转到 L1

L2: if r1 ≤ r3 goto L1

这个版本可能更快,因为每次迭代只包含一个条件分支,而不是顶部有一个条件分支,底部有一个无条件分支。(我们将在练习 C-17.4 中考虑另一个版本。)■

This version is likely to be faster, because each iteration contains a single conditional branch, rather than a conditional branch at the top and an unconditional branch at the bottom. (We will consider yet another version in Exercise C-17.4.) ■

请注意,这两种翻译都采用了从根本上讲具有方向性的循环结束测试:如图所示,它们假设i的所有实现值都小于last。如果循环“朝另一个方向”进行(即,如果first > last,且step < 0),那么我们将需要使用逆测试来结束循环。为了让编译器做出正确的选择,许多语言都会限制其算术序列的通用性。通常,step需要是一个编译时常量。Ada 实际上将选择限制为 ±1。包括 Ada 和 Pascal 在内的几种语言都要求对“向后”迭代的循环使用特殊的语法(在 Ada 中为 for i in reverse 10..1 ;在 Pascal 中为 for i := 10 downto 1)。

Note that both of these translations employ a loop-ending test that is fundamentally directional: as shown, they assume that all the realized values of i will be smaller than last. If the loop goes “the other direction”—that is, if first > last, and step < 0—then we will need to use the inverse test to end the loop. To allow the compiler to make the right choice, many languages restrict the generality of their arithmetic sequences. Commonly, step is required to be a compile-time constant. Ada actually limits the choices to ±1. Several languages, including both Ada and Pascal, require special syntax for loops that iterate “backward” (for i in reverse 10..1 in Ada; for i := 10 downto 1 in Pascal).

例 6.60

Example 6.60

具有迭代计数的for循环翻译

for loop translation with an iteration count

显然,可以生成在运行时检查step符号的代码,并据此选择测试。然而,显而易见的转换要么时间效率低,要么空间效率低。许多 Fortran 编译器采用一种可能更有吸引力的方法是预先计算迭代次数,将此迭代计数放在寄存器中,在每次迭代结束时减少寄存器,如果计数尚未为零,则分支回到循环顶部:

Obviously, one can generate code that checks the sign of step at run time, and chooses a test accordingly. The obvious translations, however, are either time or space inefficient. An arguably more attractive approach, adopted by many Fortran compilers, is to precompute the number of iterations, place this iteration count in a register, decrement the register at the end of each iteration, and branch back to the top of the loop if the count is not yet zero:

 r1 := 第一

 r1 := first

 r2 := 步骤

 r2 := step

 r3 := max( 图片(last − first + step)/step 图片, 0) –– 迭代次数

 r3 := max((last − first + step)/step, 0)   –– iteration count

    –– 注意:此计算可能需要多条指令。

    –– NB: this calculation may require several instructions.

    –– 保证结果在机器精度范围内,

    –– It is guaranteed to result in a value within the precision of the machine,

    ––但我们可能必须小心避免计算过程中溢出。

    –– but we may have to be careful to avoid overflow during its calculation.

 如果 r3 ≤ 0 则转到 L2

 if r3 ≤ 0 goto L2

L1: … –– 循环体;使用 r1 作为 i

L1: …    –– loop body; use r1 for i

 r1:= r1 + r2

 r1 := r1 + r2

 r3 := r3 − 1

 r3 := r3 − 1

 如果 r3 > 0 转到 L1

 if r3 > 0 goto L1

 我:= r1

 i := r1

L2:

L2:

例 6.61

Example 6.61

幼稚循环翻译中的“陷阱”

A “gotcha” in the naive loop translation

使用迭代计数可以避免在循环中测试step的符号。假设我们在预先计算计数时已经足够小心,它还可以避免我们在示例 6.586.59的简单翻译中忽略的一个问题:如果last接近我们机器上整数可表示的最大值,天真地将step添加到i的最终合法值可能会导致算术溢出。然后,“包装”的数字可能看起来比 last 更小(小得多!)我们可能将完美的源代码转换为无限循环。■

The use of the iteration count avoids the need to test the sign of step within the loop. Assuming we have been suitably careful in precomputing the count, it also avoids a problem we glossed over in the naive translations of Examples 6.58 and 6.59: If last is near the maximum value representable by integers on our machine, naively adding step to the final legitimate value of i may result in arithmetic overflow. The “wrapped” number may then appear to be smaller (much smaller!) than last, and we may have translated perfectly good source code into an infinite loop. ■

一些处理器(包括 Power 系列、PA-RISC 和大多数 CISC 机器)可以减少迭代次数、将其与零进行比较以及条件分支,所有这些都在一条指令中完成。对于许多循环,这会产生非常高效的代码。

Some processors, including the Power family, PA-RISC, and most CISC machines, can decrement the iteration count, test it against zero, and conditionally branch, all in a single instruction. For many loops this results in very efficient code.

语义复杂性

Semantic Complications

敏锐的读者可能已经注意到,使用迭代计数从根本上取决于在循环开始执行之前能够预测迭代次数。虽然这种预测在包括 Fortran 和 Ada 在内的许多语言中都是可能的,但在其他语言中却不可能,尤其是 C 及其后代。这种区别主要源于以下问题:for 循环构造用于迭代,还是仅仅为了使枚举变得容易?如果语言坚持枚举,那么迭代计数就可以了。如果枚举只是循环的一个可能目的 - 更具体地说,如果迭代次数或索引值序列可能会因执行前几次迭代而发生变化 - 那么我们可能需要使用更通用的实现,类似于示例6.59,如果需要,可以进行修改以处理终止测试方向的动态发现。

The astute reader may have noticed that use of an iteration count is fundamentally dependent on being able to predict the number of iterations before the loop begins to execute. While this prediction is possible in many languages, including Fortran and Ada, it is not possible in others, notably C and its descendants. The difference stems largely from the following question: is the for loop construct only for iteration, or is it simply meant to make enumeration easy? If the language insists on enumeration, then an iteration count works fine. If enumeration is only one possible purpose for the loop—more specifically, if the number of iterations or the sequence of index values may change as a result of executing the first few iterations—then we may need to use a more general implementation, along the lines of Example 6.59, modified if necessary to handle dynamic discovery of the direction of the terminating test.

设计与实现

Design & Implementation

6.7 数值不精确

6.7 Numerical imprecision

Fortran 77 对 Fortran IV 的 do 循环进行了许多更改,其中包括允许循环的索引、边界和步长为浮点数,而不仅仅是整数。有趣的是,此功能在 Fortran 90 中被从语言中删除。

Among its many changes to the do loop of Fortran IV, Fortran 77 allowed the index, bounds, and step size of the loop to be floating-point numbers, not just integers. Interestingly, this feature was taken back out of the language in Fortran 90.

实数序列的问题在于,当值彼此接近时,有限的精度可能导致比较(例如,索引和边界之间的比较)产生意外的甚至是与实现相关的结果。应该

The problem with real-number sequences is that limited precision can cause comparisons (e.g., between the index and the bound) to produce unexpected or even implementation-dependent results when the values are close to one another. Should

对于 x := 1.0 至 2.0,乘以 1.0 / 3.0

for x := 1.0 to 2.0 by 1.0 / 3.0

执行三次迭代还是四次迭代?这取决于 1.0 / 3.0 是向上还是向下舍入。Fortran 90 的设计者似乎认为这种歧义在哲学上与有限枚举的思想不一致。想要迭代浮点值的程序员必须在预测试或后测试循环中使用显式比较(第 6.5.5 节)。

execute three iterations or four? It depends on whether 1.0 / 3.0 is rounded up or down. The Fortran 90 designers appear to have decided that such ambiguity is philosophically inconsistent with the idea of finite enumeration. The programmer who wants to iterate over floating-point values must use an explicit comparison in a pretest or post-test loop (Section 6.5.5).

在要求枚举和(仅仅)允许枚举之间的选择体现在几个具体问题上:

The choice between requiring and (merely) enabling enumeration manifests itself in several specific questions:

1. 除了通过枚举机制之外,还能通过其他任何方式控制进入或离开循环吗?

1. Can control enter or leave the loop in any way other than through the enumeration mechanism?

2. 如果循环体修改了用于计算循环结束界限的变量,会发生什么情况?

2. What happens if the loop body modifies variables that were used to compute the end-of-loop bound?

3. 如果循环体修改了索引变量本身会发生什么情况?

3. What happens if the loop body modifies the index variable itself?

4. 循环结束后程序是否可以读取索引变量,如果可以,那么它的值是什么?

4. Can the program read the index variable after the loop has completed, and if so, what will its value be?

问题 (1) 和 (2) 相对容易解决。大多数语言允许break/exit语句提前退出for 循环。Fortran IV 允许goto跳转循环,但这通常被视为语言缺陷;Fortran 77 和大多数其他语言禁止此类跳转。同样,大多数语言(但不包括 C;参见第 6.5.2 节)规定在第一次迭代之前只计算一次边界,并将其保存在临时位置。对用于计算边界的变量的后续更改不会影响循环迭代的次数。

Questions (1) and (2) are relatively easy to resolve. Most languages allow a break/exit statement to leave a for loop early. Fortran IV allowed a goto to jump into a loop, but this was generally regarded as a language flaw; Fortran 77 and most other languages prohibit such jumps. Similarly, most languages (but not C; see Section 6.5.2) specify that the bound is computed only once, before the first iteration, and kept in a temporary location. Subsequent changes to variables used to compute the bound have no effect on how many times the loop iterates.

例 6.62

Example 6.62

在for循环中更改索引

Changing the index in a for loop

问题 (3) 和 (4) 更难。假设我们写(不以任何特定语言)

Questions (3) and (4) are more difficult. Suppose we write (in no particular language)

对于 i := 1 至 10,乘以 2

for i := 1 to 10 by 2

 

 

  如果 i = 3

  if i = 3

   我:= 6

   i := 6

在i = 3迭代结束时会发生什么?下一次迭代应该是i = 5(循环头中指定的算术序列的下一个元素)、i = 8(比 6 大 2),还是i = 7(序列中 6 之后的下一个值)?我们可以为这些选项中的每一个想象出合理的论据。为了避免选择的需要,许多语言禁止在循环体内更改循环索引。Fortran 将这项禁令作为程序员的纪律问题:实现不需要捕获错误更新。Pascal 提供了一套精心设计的保守规则 [ Int90,第 6.8.3.9 节],允许编译器捕获所有可能的更新。这些规则很复杂,因为索引变量是在循环外部声明的;即使它没有作为参数传递,也可能对从循环调用的子例程可见。■

What should happen at the end of the i = 3 iteration? Should the next iteration have i = 5 (the next element of the arithmetic sequence specified in the loop header), i = 8 (2 more than 6), or even conceivably i = 7 (the next value of the sequence after 6)? One can imagine reasonable arguments for each of these options. To avoid the need to choose, many languages prohibit changes to the loop index within the body of the loop. Fortran makes the prohibition a matter of programmer discipline: the implementation is not required to catch an erroneous update. Pascal provided an elaborate set of conservative rules [Int90, Sec. 6.8.3.9] that allowed the compiler to catch all possible updates. These rules were complicated by the fact that the index variable was declared outside the loop; it might be visible to subroutines called from the loop even if it was not passed as a parameter. ■

例 6.63

Example 6.63

检查for循环后的索引

Inspecting the index after a for loop

如果控制使用break/exit退出循环,索引的自然值似乎是退出时当前的值。另一方面,对于“正常”终止,自然值似乎是第一个超出循环界限的值。当然,这是示例 6.59的实现将产生的值。不幸的是,正如我们在示例 6.60中指出的那样,某些循环的“下一个”值可能超出了整数精度的范围。对于其他循环,它可能在语义上无效:

If control escapes the loop with a break/exit, the natural value for the index would seem to be the one that was current at the time of the escape. For “normal” termination, on the other hand, the natural value would seem to be the first one that exceeds the loop bound. Certainly that is the value that will be produced by the implementation of Example 6.59. Unfortunately, as we noted in Example 6.60, the “next” value for some loops maybe outside the range of integer precision. For other loops, it maybe semantically invalid:

c : 'a'..'z' –– 字符子范围

c : ‘a’..‘z’   –– character subrange

对于 c := 'a' 到 'z' 执行

for c := ‘a’ to ‘z’ do

 

 

–– 'z' 后面是什么?

–– what comes after ‘z’?

从实现角度来看,要求循环后值始终是最后一次迭代的索引是没有吸引力:它会迫使我们用在每次迭代中都有额外分支指令的翻译来替换示例 6.59 :

Requiring the post-loop value to always be the index of the final iteration is unattractive from an implementation perspective: it would force us to replace Example 6.59 with a translation that has an extra branch instruction in every iteration:

 r1:='a'

 r1 := ‘a’

 r2 := ‘z’

 r2 := ‘z’

 如果 r1 > r2,则转到 L3 –– 代码改进者可能会删除此测试,

 if r1 > r2 goto L3  –– Code improver may remove this test,

     –– 因为 'a' 和 'z' 是常数。

     –– since ‘a’ and ‘z’ are constants.

L1: … –– 循环体;使用 r1 作为 i

L1: …        –– loop body; use r1 for i

 如果 r1 = r2 转到 L2

 if r1 = r2 goto L2

 r1:=r1+1

 r1 := r1 + 1

 转到 L1

 goto L1

L2:我:= r1

L2: i := r1

三级:

L3:

当然,如果算术溢出可能会干扰对终止条件的测试,编译器无论如何都必须生成此类代码(或使用迭代计数)。为了允许编译器在所有情况下使用最快的正确实现,包括 Fortran 90 和 Pascal 在内的几种语言都规定索引的值在循环结束后未定义。■

Of course, the compiler must generate this sort of code in any event (or use an iteration count) if arithmetic overflow may interfere with testing the terminating condition. To permit the compiler to use the fastest correct implementation in all cases, several languages, including Fortran 90 and Pascal, say that the value of the index is undefined after the end of the loop. ■

Algol W 和 Algol 68 率先提出了一种解决索引修改问题和循环后值问题的有吸引力的解决方案,随后被 Ada、Modula 3 和许多其他语言采用。在这些语言中,循环的头部被认为包含索引的声明。其类型是从循环的边界推断出来的,其范围是循环的主体。由于索引在循环外部不可见,因此其值不是问题。当然,程序员不能将索引命名为与循环内必须访问的任何变量相同的名称,但这是一个严格的本地问题:它不会对循环外产生任何影响。

An attractive solution to both the index modification problem and the post-loop value problem was pioneered by Algol W and Algol 68, and subsequently adopted by Ada, Modula 3, and many other languages. In these, the header of the loop is considered to contain a declaration of the index. Its type is inferred from the bounds of the loop, and its scope is the loop's body. Because the index is not visible outside the loop, its value is not an issue. Of course, the programmer must not give the index the same name as any variable that must be accessed within the loop, but this is a strictly local issue: it has no ramifications outside the loop.

6.5.2 组合循环

6.5.2 Combination Loops

Algol 60 提供了一个单循环结构,该结构包含了更现代的枚举和逻辑控制循环的属性。它允许程序员指定任意数量的“枚举器”,每个枚举器可以是单个值、类似于现代枚举控制循环的值范围或具有终止条件的表达式。Common Lisp 提供了更强大的功能,具有四组独立的子句,用于初始化索引变量(其中可能有任意数量)、测试循环终止(以多种方式中的任何一种)、评估主体表达式以及在循环终止时进行清理。

Algol 60 provided a single loop construct that subsumed the properties of more modern enumeration and logically controlled loops. It allowed the programmer to specify an arbitrary number of “enumerators,” each of which could be a single value, a range of values similar to those of modern enumeration-controlled loops, or an expression with a terminating condition. Common Lisp provides an even more powerful facility, with four separate sets of clauses, to initialize index variables (of which there may be an arbitrary number), test for loop termination (in any of several ways), evaluate body expressions, and clean up at loop termination.

例 6.64

Example 6.64

C 中的组合(for )循环

Combination (for) loop in C

C 及其后续版本中出现了一种更简单的组合循环形式。从语义上讲,C for 循环是逻辑控制的。然而,它的设计是为了使枚举变得容易。我们的 Modula-2 示例

A much simpler form of combination loop appears in C and its successors. Semantically, the C for loop is logically controlled. It was designed, however, to make enumeration easy. Our Modula-2 example

FOR i := 第一 TO 最后 BY 步骤 DO

FOR i := first TO last BY step DO

 

 

结尾

END

通常在 C 中写为

would usually be written in C as

对于(i = 第一个;i <= 最后一个;i += 步骤){

for (i = first; i <= last; i += step) {

 

 

}

}

除了一些特殊情况外,C 将其定义为等同于

With caveats for a few special cases, C defines this to be equivalent to

{

{

 i = 第一;

 i = first;

 while (i <= 最后一个) {

 while (i <= last) {

  

  

  我+ =步骤;

  i += step;

 }

 }

}

}

此定义意味着程序员有责任担心溢出对终止条件测试的影响。这还意味着索引和终止条件中包含的任何变量都可以由循环体或其调用的子例程修改,并且这些更改影响循环控制。这也是程序员的责任。

This definition means that it is the programmer's responsibility to worry about the effect of overflow on testing of the terminating condition. It also means that both the index and any variables contained in the terminating condition can be modified by the body of the loop, or by subroutines it calls, and these changes will affect the loop control. This, too, is the programmer's responsibility.

for 循环头中的三个子句中的任何一个都可以为空(如果缺失,则条件被视为真)。或者,子句可以由逗号分隔的表达式序列组成。C for循环相对于其 while 循环等效项的优势在于紧凑性和清晰度。特别是,影响控制流位于头部内。在while循环中,必须读取循环顶部和底部才能知道发生了什么。

Any of the three clauses in the for loop header can be null (the condition is considered true if missing). Alternatively, a clause can consist of a sequence of comma-separated expressions. The advantage of the C for loop over its while loop equivalent is compactness and clarity. In particular, all of the code affecting the flow of control is localized within the header. In the while loop, one must read both the top and the bottom of the loop to know what is going on.

设计与实现

Design & Implementation

6.8 for循环

6.8 for Loops

现代for循环反映了语义和实现挑战的影响。语义挑战包括循环内部循环索引或边界的更改、索引变量的范围(以及循环外部的其值(如果有))以及进入或离开循环的goto。实现挑战包括浮点值的不精确性、循环底部测试的方向以及迭代范围末尾的溢出。C 的“组合循环”(第6.5.2 节)将这些挑战的责任从编译器转移到应用程序中。

Modern for loops reflect the impact of both semantic and implementation challenges. Semantic challenges include changes to loop indices or bounds from within the loop, the scope of the index variable (and its value, if any, outside the loop), and gotos that enter or leave the loop. Implementation challenges include the imprecision of floating-point values, the direction of the bottom-of-loop test, and overflow at the end of the iteration range. The “combination loops” of C (Section 6.5.2) move responsibility for these challenges out of the compiler and into the application program.

例 6.65

Example 6.65

带有本地索引的C for循环

C for loop with a local index

虽然 C for循环的逻辑迭代语义消除了循环结束后索引变量值的任何歧义,但通过在头的初始化子句中声明索引,将索引设为循环主体的本地索引可能仍然很方便。在示例 6.64中,变量i必须在周围范围内声明。如果我们改为写

While the logical iteration semantics of the C for loop eliminate any ambiguity about the value of the index variable after the end of the loop, it may still be convenient to make the index local to the body of the loop, by declaring it in the header's initialization clause. In Example 6.64, variable i must be declared in the surrounding scope. If we instead write

对于(int i = 第一个;i <= 最后一个;i += 步骤){

for (int i = first; i <= last; i += step) {

 

 

}

}

那么i在外部就不可见了。但是,它仍然容易受到循环内(故意或意外)修改的影响。■

then i will not be visible outside. It will still, however, be vulnerable to (deliberate or accidental) modification within the loop. ■

6.5.3 迭代器

6.5.3 Iterators

在我们迄今为止看到的所有示例中(可能除了 Algol 60、Common Lisp 或 C 的组合循环),for循环都会迭代算术序列的元素。但是,一般来说,我们可能希望迭代任何明确定义的集合(在面向对象代码中通常称为集合或容器类的实例)的元素。Clu 引入了一种优雅的迭代器机制(在 Python、Ruby 和 C# 中也有)来精确地做到这一点。Euclid 和几种较新的语言(尤其是 C++、Java 和 Ada 2012)为迭代器对象(有时称为枚举器)定义了一个标准接口,这些接口同样易于使用,但编写起来不那么容易。相反,Icon 提供了迭代器的泛化,称为生成器,它将枚举与回溯搜索相结合。7

In all of the examples we have seen so far (with the possible exception of the combination loops of Algol 60, Common Lisp, or C), a for loop iterates over the elements of an arithmetic sequence. In general, however, we may wish to iterate over the elements of any well-defined set (what are often called collections, or instances of a container class, in object-oriented code). Clu introduced an elegant iterator mechanism (also found in Python, Ruby, and C#) to do precisely that. Euclid and several more recent languages, notably C++, Java, and Ada 2012, define a standard interface for iterator objects (sometimes called enumerators) that are equally easy to use, but not as easy to write. Icon, conversely, provides a generalization of iterators, known as generators, that combines enumeration with backtracking search.7

真正的迭代器

True Iterators

例 6.66

Example 6.66

Python 中的简单迭代器

Simple iterator in Python

Clu、Python、Ruby 和 C# 允许任何容器抽象提供枚举其项的迭代器。迭代器类似于允许包含yield语句的子例程,每个语句都会产生一个循环索引值。然后设计For循环以包含对迭代器的调用。Modula-2 片段

Clu, Python, Ruby, and C# allow any container abstraction to provide an iterator that enumerates its items. The iterator resembles a subroutine that is permitted to contain yield statements, each of which produces a loop index value. For loops are then designed to incorporate a call to an iterator. The Modula-2 fragment

FOR i := 第一 TO 最后 BY 步骤 DO

FOR i := first TO last BY step DO

 

 

结尾

END

在 Python 中写成如下:

would be written as follows in Python:

对于范围内的 i(第一个,最后一个,步骤):

for i in range(first, last, step):

 

 

这里 range 是一个内置迭代器,以step为增量生成从firstfirst + 图片(last — first )/step 图片 × step的整数。■

Here range is a built-in iterator that yields the integers from first to first + (last — first )/step × step in increments of step. ■

调用迭代器时,它会计算循环的第一个索引值,并通过执行yield语句将其返回给主程序。yield的行为类似于return,不同之处在于,当完成循环的第一次迭代后将控制权转回迭代器时,迭代器会从上次停止的地方继续执行,而不是从代码的开头继续执行。当迭代器没有更多元素可产生时,它只会返回(没有值),从而终止循环。

When called, the iterator calculates the first index value of the loop, which it returns to the main program by executing a yield statement. The yield behaves like return, except that when control transfers back to the iterator after completion of the first iteration of the loop, the iterator continues where it last left off—not at the beginning of its code. When the iterator has no more elements to yield it simply returns (without a value), thereby terminating the loop.

实际上,迭代器是一个单独的控制线程,具有自己的程序计数器,其执行与它提供索引值的for循环的执行交错​​。8迭代机制用于将枚举元素所需的算法与使用这些元素的代码“分离”。

In effect, an iterator is a separate thread of control, with its own program counter, whose execution is interleaved with that of the for loop to which it supplies index values.8 The iteration mechanism serves to “decouple” the algorithm required to enumerate elements from the code that uses those elements.

例 6.67

Example 6.67

用于树枚举的 Python 迭代器

Python iterator for tree enumeration

范围迭代器在 Python 中是预定义的。作为一个更具说明性的示例,请考虑二叉树中存储的值的先序枚举。图 6.5显示了用于此任务的 Python 迭代器。从for循环的头部调用,它产生根节点中的值(如果有)用于第一次迭代,然后递归调用自身两次,以枚举左子树和右子树中的值。■

The range iterator is predefined in Python. As a more illustrative example, consider the preorder enumeration of values stored in a binary tree. A Python iterator for this task appears in Figure 6.5. Invoked from the header of a for loop, it yields the value in the root node (if any) for the first iteration and then calls itself recursively, twice, to enumerate values in the left and right subtrees. ■

06-05-9780124104099
图 6.5 用于二叉树节点预序枚举的 Python 迭代器。由于 Python 是动态类型的,因此此代码适用于支持插入、查找等所需操作的任何数据(可能只是 <)。在静态类型语言中,BinTree类需要是通用的。

迭代器对象

Iterator Objects

正如在大多数命令式语言中实现的那样,迭代既涉及一种特殊形式的for循环,又涉及一种枚举循环值的机制。这些概念可以分开。Euclid、C++、Java 和 Ada 2012 都提供了枚举控制的循环,让人想起 Python 的循环。但是,它们没有 Yield 语句,也没有单独的线程式上下文来枚举值;相反,迭代器是一个普通对象(在面向对象的意义上),它提供初始化、生成下一个索引值和测试完成的方法。在调用之间,迭代器的状态必须保存在对象的数据成员中。

As realized in most imperative languages, iteration involves both a special form of for loop and a mechanism to enumerate values for the loop. These concepts can be separated. Euclid, C++, Java, and Ada 2012 all provide enumeration-controlled loops reminiscent of those of Python. They have no yield statement, however, and no separate thread-like context to enumerate values; rather, an iterator is an ordinary object (in the object-oriented sense of the word) that provides methods for initialization, generation of the next index value, and testing for completion. Between calls, the state of the iterator must be kept in the object's data members.

例 6.68

Example 6.68

用于树枚举的 Java 迭代器

Java iterator for tree enumeration

图 6.6包含图 6.5BinTree类的 Java 等效代码。根据此代码,我们可以编写

Figure 6.6 contains the Java equivalent of the BinTree class of Figure 6.5. Given this code, we can write

编号:F06-06-9780124104099
图 6.6 二叉树节点的先序枚举的 Java 代码。嵌套的TreeIterator类使用显式 Stack 对象(借用自标准库)来跟踪尚未枚举节点的子树。Java 泛型(指定为BinTree、Stack、IteratorIterable的 <T> 类型参数)允许 next 返回适当类型的对象,而不是未分化的 Object 。remove方法是Iterator接口的一部分,因此必须提供,即使只是作为占位符

BinTree<Integer> myTree = …

BinTree<Integer> myTree = …

对于(整数 i:myTree){

for (Integer i : myTree) {

 系统.out.println(i);

 System.out.println(i);

}

}

这里的循环是

The loop here is syntactic sugar for

对于(迭代器 <Integer> it = myTree.iterator(); it.hasNext();){

for (Iterator<Integer> it = myTree.iterator(); it.hasNext();) {

 整数 i = it.next();

 Integer i = it.next();

 系统.out.println(i);

 System.out.println(i);

}

}

更简洁版本的循环中冒号后面的表达式必须是支持标准Iterable接口的对象。此接口包含一个返回Iterator对象的iterator()方法。■

The expression following the colon in the more concise version of the loop must be an object that supports the standard Iterable interface. This interface includes an iterator() method that returns an Iterator object. ■

例 6.69

Example 6.69

C++11 中的迭代

Iteration in C++11

C++ 采用了一种相关但略有不同的方法。通过适当的定义,上例中的 Java for循环在 C++11 中可以写成如下形式:

C++ takes a related but somewhat different approach. With appropriate definitions, the Java for loop of the previous example could be written as follows in C++11:

tree_node*my_tree=…

tree_node* my_tree = …

对于(int n:*my_tree){

for (int n : *my_tree) {

 cout<<n<<“\n”;

 cout << n << “\n”;

}

}

设计与实现

Design & Implementation

6.9 “真”迭代器和迭代器对象

6.9 “True” iterators and iterator objects

虽然C++ 和 Java 的迭代器库机制非常有用,但值得强调的是,它们并不是Clu、Python、Ruby 和 C# 中的“真正”迭代器的功能对等物。它们的主要限制是需要以显式数据结构的形式维护所有中间状态,而不是在可恢复执行上下文的程序计数器和局部变量中。

While the iterator library mechanisms of C++ and Java are highly useful, it is worth emphasizing that they are not the functional equivalents of “true” iterators, as found in Clu, Python, Ruby, and C#. Their key limitation is the need to maintain all intermediate state in the form of explicit data structures, rather than in the program counter and local variables of a resumable execution context.

这个循环是

This loop is syntactic sugar for

对于(tree_node::iterator it = my_tree->begin();

for (tree_node::iterator it = my_tree->begin();

    它 != my_tree->end(); ++它) {

    it != my_tree->end(); ++it) {

 int n = *它;

 int n = *it;

 cout<<n<<“\n”;

 cout << n << “\n”;

}

}

Java 迭代器具有根据需要生成集合中连续元素的方法(并指示何时不再有元素),而 C++ 迭代器旨在充当一种特殊的指针。标准库中的支持例程利用语言异常灵活的运算符重载和引用机制来重新定义比较 (!=)、增量 (++)、取消引用 (*) 等,使得对集合元素的迭代看起来非常像使用指针算法遍历传统数组(“C 中的指针和数组”,第 8.5.1 节)。

Where a Java iterator has methods to produce successive elements of a collection on demand (and to indicate when there are no more), a C++ iterator is designed to act as a special kind of pointer. Support routines in the standard library leverage the language's unusually flexible operator overloading and reference mechanisms to redefine comparison (!=), increment (++), dereference (*), and so on in a way that makes iterating over the elements of a collection look very much like using pointer arithmetic to traverse a conventional array (“Pointers and Arrays in C,” Section 8.5.1).

与 Java 示例一样,迭代器封装了查找集合中连续元素以及确定何时不再有元素所需的所有状态。要获取当前元素,我们使用 * 或 -> 运算符“取消引用”迭代器。迭代器的初始值由集合的 begin 方法生成。要前进到下一个元素,我们使用增量 (++) 运算符。end 方法返回一个特殊的迭代器,它“指向集合末尾以外的地方”。当集合耗尽时,增量 (++) 运算符必须返回一个引用,该引用测试与此特殊迭代器相等。■

As in the Java example, iterator it encapsulates all the state needed to find successive elements of the collection, and to determine when there are no more. To obtain the current element, we “dereference” the iterator, using the * or -> operators. The initial value of the iterator is produced by a collection's begin method. To advance to the following element, we use the increment (++) operator. The end method returns a special iterator that “points beyond the end” of the collection. The increment (++) operator must return a reference that tests equal to this special iterator when the collection has been exhausted. ■

由于运算符重载、变量的值模型(需要显式引用和指针)以及缺少垃圾收集,实现 C++ 树迭代器的代码比图 6.6的 Java 版本稍微混乱一些。我们将细节留给练习 6.19

Code to implement our C++ tree iterator is somewhat messier than the Java version of Figure 6.6, due to operator overloading, the value model of variables (which requires explicit references and pointers), and the lack of garbage collection. We leave the details to Exercise 6.19.

使用一等函数进行迭代

Iterating with First-Class Functions

例 6.70

Example 6.70

将“循环体”传递给 Scheme 中的迭代器

Passing the “loop body” to an iterator in Scheme

在函数式语言中,能够“内联”指定函数,这有利于形成一种编程习惯,即循环体写成函数,循环索引作为参数。然后,此函数作为最后一个参数传递给迭代器,迭代器本身也是一个函数。在 Scheme 中,我们可以这样写

In functional languages, the ability to specify a function “in line” facilitates a programming idiom in which the body of a loop is written as a function, with the loop index as an argument. This function is then passed as the final argument to an iterator, which is itself a function. In Scheme we might write

(定义 uptoby

(define uptoby

 (lambda(低高步骤f)

 (lambda (low high step f)

  (若(<= 低高)

  (if (<= low high)

   (开始

   (begin

    (流动)

    (f low)

    (uptoby (+ 低步) 高步 f))

    (uptoby (+ low step) high step f))

   '())))

   '())))

然后我们可以将前 50 个奇数相加,如下所示:

We could then sum the first 50 odd numbers as follows:

(设((和 0))

(let ((sum 0))

 (最多 1 100 2

 (uptoby 1 100 2

   (λ(i)

   (lambda (i)

    (设置!总和(+ 总和 i))))

    (set! sum (+ sum i))))

 总和)⇒ 2500

 sum)    ⇒ 2500

这里循环体(set! sum (+ sum i))是一个赋值。这里使用符号(不是 Scheme 的一部分)表示“求值为”。■

Here the body of the loop, (set! sum (+ sum i)), is an assignment. The symbol (not a part of Scheme) is used here to mean “evaluates to.” ■

例 6.71

Example 6.71

Smalltalk 中的块迭代

Iteration with blocks in Smalltalk

我们在C-10.7.1 节中讨论的 Smalltalk支持类似的习语:

Smalltalk, which we consider in Section C-10.7.1, supports a similar idiom:

总和 <- 0。

sum <- 0.

1 至: 100 乘以: 2 执行:

1 to: 100 by: 2 do:

 [:i | 总和 <- 总和 + i]

 [:i | sum <- sum + i]

就像lambda一样 Scheme 中的表达式,Smalltalk 中的方括号块创建一个一等函数,然后我们将其作为参数传递给 to: by: do: 迭代器。迭代器重复调用该函数,将索引变量i的连续值作为参数传递。■

Like a lambda expression in Scheme, a square-bracketed block in Smalltalk creates a first-class function, which we then pass as argument to the to: by: do: iterator. The iterator calls the function repeatedly, passing successive values of the index variable i as argument. ■

例 6.72

Example 6.72

在 Ruby 中使用 procs 进行迭代

Iterating with procs in Ruby

Ruby 中的迭代器也类似,具有函数语义,但语法让人联想到 Python 或 C#。我们在 Ruby 中的 uptoby 迭代器可以写成如下形式:

Iterators in Ruby are also similar, with functional semantics but syntax reminiscent of Python or C#. Our uptoby iterator in Ruby could be written as follows:

def uptoby(第一个,最后一个,inc)

def uptoby(first, last, inc)

 while first <= last 执行

 while first <= last do

  先让步

  yield first

  第一 += 增加

  first += inc

 结尾

 end

结尾

end

总和 = 0

sum = 0

uptoby(1, 100, 2) { |i| 总和 += i }

uptoby(1, 100, 2) { |i| sum += i }

把总数 ⇒ 2500

puts sum    ⇒ 2500

此代码被定义为

This code is defined as syntactic sugar for

def uptoby(第一个,最后一个,inc,块)

def uptoby(first, last, inc, block)

 while first <= last 执行

 while first <= last do

  block.call(第一个)

  block.call(first)

  第一 += 增加

  first += inc

 结尾

 end

结尾

end

总和 = 0

sum = 0

uptoby(1,100,2,Proc.new {| i| sum += i})

uptoby(1, 100, 2, Proc.new { |i| sum += i })

放总和

puts sum

当一个块(用括号或do… end分隔)跟在函数调用的参数列表后面时,Ruby 会将表示该块的闭包(“proc”)作为隐式额外参数传递给函数。在函数主体中,yield被定义为对函数最后一个参数的调用,该参数必须是 proc,无需明确声明。

When a block, delimited by braces or do… end, follows the parameter list of a function invocation, Ruby passes a closure representing the block (a “proc”) as an implicit extra argument to the function. Within the body of the function, yield is defined as a call to the function's last parameter, which must be a proc, and need not be explicitly declared.

为了更加方便,Ruby 的所有集合对象(数组、范围、映射和集合)都支持名为each 的方法,该方法将为集合中的每个元素调用一个块。要对前 100 个整数求和(步长不为 2),我们可以说

For added convenience, all of Ruby's collection objects (arrays, ranges, mappings, and sets) support a method named each that will invoke a block for every element of the collection. To sum the first 100 integers (without the step size of 2), we could say

总和 = 0

sum = 0

(1..100).每个{| i |总和+ = i}

(1..100).each { |i| sum += i }

把总数 ⇒ 5050

puts sum   ⇒ 5050

此代码作为常规 for 循环语法的定义,它是进一步的语法糖:

This code serves as the definition of conventional for-loop syntax, which is further syntactic sugar:

总和 = 0

sum = 0

对于我在(1..100)中做

for i in (1..100) do

 总和 += i

 sum += i

结尾

end

放总和

puts sum

在Lisp 和Scheme 中,我们可以利用延续(第6.2.2 节)和惰性求值(第6.6.2 节)来定义类似的语法糖;我们在练习6.346.35中考虑了这种可能性。■

In Lisp and Scheme, one can define similar syntactic sugar using continuations (Section 6.2.2) and lazy evaluation (Section 6.6.2); we consider this possibility in Exercises 6.34 and 6.35. ■

不使用迭代器进行迭代

Iterating without Iterators

例 6.73

Example 6.73

在 C 中模仿迭代器

Imitating iterators in C

在既没有真正的迭代器也没有迭代器对象的语言中,我们仍然可以通过采用适当的编程约定将集合的枚举与元素的实际使用分离开来。例如,在 C 语言中,我们可以定义一个tree_iter类型和相关函数,它们可以在循环中使用,如下所示:

In a language with neither true iterators nor iterator objects, we can still decouple the enumeration of a collection from actual use of the elements by adopting appropriate programming conventions. In C, for example, we might define a tree_iter type and associated functions that could be used in a loop as follows:

bin_tree *我的树;

bin_tree *my_tree;

树_iter ti;

tree_iter ti;

for (ti_create(my_tree, &ti); !ti_done(ti); ti_next(&ti)) {

for (ti_create(my_tree, &ti); !ti_done(ti); ti_next(&ti)) {

 二叉树 *n = ti_val(ti);

 bin_tree *n = ti_val(ti);

 

 

}

}

ti_删除(&ti);

ti_delete(&ti);

此代码与更结构化的替代方案之间有两个主要区别:(1)循环的语法不太优雅(并且可以说更容易出现意外错误),(2)迭代器的代码只是一种类型和一些相关函数——C 没有提供抽象机制将它们组合在一起作为模块或类。通过为迭代器抽象提供标准接口,面向对象语言促进了操作整个集合的高阶机制的设计:对它们进行排序、合并、查找它们的交点或差异,等等。我们将 tree_iter各种ti_函数的C 代码留给练习 6.20。■

There are two principal differences between this code and the more structured alternatives: (1) the syntax of the loop is a good bit less elegant (and arguably more prone to accidental errors), and (2) the code for the iterator is simply a type and some associated functions—C provides no abstraction mechanism to group them together as a module or a class. By providing a standard interface for iterator abstractions, object-oriented languages facilitate the design of higher-order mechanisms that manipulate whole collections: sorting them, merging them, finding their intersection or difference, and so on. We leave the C code for tree_iter and the various ti_ functions to Exercise 6.20. ■

6.5.4 Icon 中的生成器

6.5.4 Generators in Icon

Icon 概括了迭代器的概念,提供了一种生成器机制,使得嵌入的任何表达式能够根据需要枚举多个值。

Icon generalizes the concept of iterators, providing a generator mechanism that causes any expression in which it is embedded to enumerate multiple values on demand.

06-02-9780124104099更深入地

IN MORE DEPTH

我们在配套网站上更详细地讨论了 Icon 生成器。该语言的枚举控制循环,即 every 循环,不仅可以包含生成器,而是任何包含生成器的表达式。生成器还可以用于if语句之类的结构中,如果任何生成的值使条件为真,它将执行其嵌套代码,自动搜索所有可能性。当生成器嵌套时,Icon 会探索生成值的所有可能组合,甚至会在必要时回溯以撤消不成功的控制流分支或分配。

We consider Icon generators in more detail on the companion site. The language's enumeration-controlled loop, the every loop, can contain not only a generator, but any expression that contains a generator. Generators can also be used in constructs like if statements, which will execute their nested code if any generated value makes the condition true, automatically searching through all the possibilities. When generators are nested, Icon explores all possible combinations of generated values, and will even backtrack where necessary to undo unsuccessful control-flow branches or assignments.

6.5.5 逻辑控制循环

6.5.5 Logically Controlled Loops

例 6.74

Example 6.74

Algol-W 中的while循环

while loop in Algol-W

与枚举控制循环相比,逻辑控制循环的语义细节少得多。唯一真正需要回答的问题是循环主体中终止条件的测试位置。迄今为止,最常见的方法是在每次迭代之前测试条件。Algol-W 中引入了熟悉的while循环语法:

In comparison to enumeration-controlled loops, logically controlled loops have many fewer semantic subtleties. The only real question to be answered is where within the body of the loop the terminating condition is tested. By far the most common approach is to test the condition before each iteration. The familiar while loop syntax for this was introduced in Algol-W:

while条件do语句

while condition do statement

为了使循环体成为语句列表,大多数现代语言使用显式结束关键字(例如end),或用分隔符括住循环体(例如 { …})。少数语言(尤其是 Python)使用额外的缩进级别来指示循环体。■

To allow the body of the loop to be a statement list, most modern languages use an explicit concluding keyword (e.g., end), or bracket the body with delimiters (e.g., { …}). A few languages (notably Python) indicate the body with an extra level of indentation. ■

测试后循环

Post-test Loops

例 6.75

Example 6.75

Pascal 和 Modula 中的后测试循环

Post-test loop in Pascal and Modula

有时,能够在循环底部测试终止条件是很方便的。Pascal 为这种情况引入了特殊语法,该语法在 Modula 中保留,但在 Ada 中被删除。例如,后测试循环允许我们编写

Occasionally it is handy to be able to test the terminating condition at the bottom of a loop. Pascal introduced special syntax for this case, which was retained in Modula but dropped in Ada. A post-test loop allows us, for example, to write

重复

repeat

 readln(行)

 readln(line)

直到行[1] = '$';

until line[1] = '$';

而不是

instead of

读取(行);

readln(line);

当行[1] <> '$' 执行

while line[1] <> '$' do

 读取(行);

 readln(line);

当循环体较长时,这些结构之间的差异尤其重要。请注意,后测试循环体始终至少执行一次。■

The difference between these constructs is particularly important when the body of the loop is longer. Note that the body of a post-test loop is always executed at least once. ■

例 6.76

Example 6.76

C 语言中的后测试循环

Post-test loop in C

C 提供了一个后测试循环,其条件在“另一个方向”起作用(即“while”而不是“until”):

C provides a post-test loop whose condition works “the other direction” (i.e., “while” instead of “until”):

做 {

do {

 行 = 读取行 (stdin);

 line = read_line(stdin);

} 当(line[0] != '$')时;

} while (line[0] != '$');

测试中期循环

Mid-test Loops

例 6.77

Example 6.77

C 中的break语句

break statement in C

最后,正如我们在6.2.1 节中提到的,有时在循环中间测试终止条件是合适的。在许多语言中,这种“中间测试”可以通过嵌套在条件语句中的特殊语句来完成:Ada 中的exit 、 C 中的break 、 Perl 中的last 。在6.4.2 节中,我们看到了一种不太常见的使用break来退出 C switch语句的方法。更常见的是,C 还使用break退出最近的for、whiledo循环:

Finally, as we noted in Section 6.2.1, it is sometimes appropriate to test the terminating condition in the middle of a loop. In many languages this “mid-test” can be accomplished with a special statement nested inside a conditional: exit in Ada, break in C, last in Perl. In Section 6.4.2 we saw a somewhat unusual use of break to leave a C switch statement. More conventionally, C also uses break to exit the closest for, while, or do loop:

为了 (;;) {

for (;;) {

 行 = 读取行 (stdin);

 line = read_line(stdin);

 如果(all_blanks(line))中断;

 if (all_blanks(line)) break;

 消耗_线(线);

 consume_line(line);

}

}

这里, for循环头中缺失的条件被假定为始终为真。(C 程序员传统上更喜欢这种语法,而不是等效的while (1),大概是因为它在某些早期的 C 编译器中速度更快。)■

Here the missing condition in the for loop header is assumed to always be true. (C programmers have traditionally preferred this syntax to the equivalent while (1), presumably because it was faster in certain early C compilers.) ■

例 6.78

Example 6.78

在 Ada 中退出嵌套循环

Exiting a nested loop in Ada

在某些语言中,exit语句带有可选的循环名称参数,允许控制退出嵌套循环。在 Ada 中,我们可能会写

In some languages, an exit statement takes an optional loop-name argument that allows control to escape a nested loop. In Ada we might write

外层:循环

outer: loop

 获取线(线,长度);

 get_line(line, length);

 for i in 1..length 循环

 for i in 1..length loop

  当 line(i) = '$' 时退出外部;

  exit outer when line(i) = '$';

  消耗_char(行(i));

  consume_char(line(i));

 结束循环;

 end loop;

结束循环外;

end loop outer;

例 6.79

Example 6.79

在 Perl 中退出嵌套循环

Exiting a nested loop in Perl

在 Perl 中这将是

In Perl this would be

outer: while (<>) { # 迭代输入行

outer: while (<>) { # iterate over lines of input

 foreach $c (split //) { # 遍历剩余的字符

 foreach $c (split //) { # iterate over remaining chars

  last outer if ($c =~ '\$'); # 如果看到 $ 符号则退出主循环

  last outer if ($c =~ '\$'); # exit main loop if we see a $ sign

  消耗_char($c);

  consume_char($c);

 }

 }

}

}

Java 以类似的方式扩展了 C/C++ break 语句,并在循环上带有可选标签。

Java extends the C/C++ break statement in a similar fashion, with optional labels on loops.

06-01-9780124104099检查你的理解

Check Your Understanding

28. 描述枚举控制循环实现中的三个微妙之处。

28. Describe three subtleties in the implementation of enumeration-controlled loops.

29. 为什么大多数语言不允许枚举控制循环的边界或增量为浮点数?

29. Why do most languages not allow the bounds or increment of an enumeration-controlled loop to be floating-point numbers?

30. 为什么许多语言要求枚举控制循环的步长为编译时常量?

30. Why do many languages require the step size of an enumeration-controlled loop to be a compile-time constant?

31. 描述“迭代计数”循环的实现。它解决了哪些问题?

31. Describe the “iteration count” loop implementation. What problem(s) does it solve?

32. 将索引变量设为其控制的循环本地变量有哪些优点?

32. What are the advantages of making an index variable local to the loop it controls?

33.  C 有枚举控制循环吗?解释一下。

33. Does C have enumeration-controlled loops? Explain.

34. 什么是集合容器实例)?

34. What is a collection (a container instance)?

35. 解释真正的迭代器和迭代器对象之间的区别。

35. Explain the difference between true iterators and iterator objects.

36. 列举迭代器对象相对于 C 语言等编程约定的两个优点。

36. Cite two advantages of iterator objects over the use of programming conventions in a language like C.

37. 描述具有一流功能的语言中通常采用的迭代方法。

37. Describe the approach to iteration typically employed in languages with first-class functions.

38. 举一个例子,说明中测试循环比前测试或后测试循环产生更优雅的代码。

38. Give an example in which a mid-test loop results in more elegant code than does a pretest or post-test loop.

6.6 递归

6.6 Recursion

与迄今为止讨论的控制流机制不同,递归不需要特殊语法。在任何提供子程序(特别是函数)的语言中,所需的只是允许函数调用自身,或调用其他函数,然后依次调用它们。大多数程序员在数据结构类中了解到,递归和(逻辑控制的)迭代提供了同样强大的计算函数方法:任何迭代算法都可以自动重写为递归算法,反之亦然。我们将在下面的第一小节中更详细地比较迭代和递归。在下一小节中,我们将考虑将未求值的表达式传递给函数的可能性。虽然由于实现成本,通常不建议这样做,但这种技术有时可以让我们为仅在可能输入的子集上定义的函数或探索逻辑上无限的数据结构的函数编写优雅的代码。

Unlike the control-flow mechanisms discussed so far, recursion requires no special syntax. In any language that provides subroutines (particularly functions), all that is required is to permit functions to call themselves, or to call other functions that then call them back in turn. Most programmers learn in a data structures class that recursion and (logically controlled) iteration provide equally powerful means of computing functions: any iterative algorithm can be rewritten, automatically, as a recursive algorithm, and vice versa. We will compare iteration and recursion in more detail in the first subsection below. In the following subsection we will consider the possibility of passing unevaluated expressions into a function. While usually inadvisable, due to implementation cost, this technique will sometimes allow us to write elegant code for functions that are only defined on a subset of the possible inputs, or that explore logically infinite data structures.

6.6.1 迭代和递归

6.6.1 Iteration and Recursion

例 6.80

Example 6.80

一个“自然迭代”的问题

A “naturally iterative” problem

正如我们在第 3.2 节中提到的,Fortran 77 和某些其他语言不允许递归。一些函数式语言不允许迭代。然而,大多数现代语言都提供这两种机制。在命令式语言中,迭代在某种意义上是两者中更“自然”的一种,因为它基于对变量的重复修改。在函数式语言,因为它不改变变量。归根结底,在什么情况下使用哪种方法主要取决于个人喜好。要计算总和,

As we noted in Section 3.2, Fortran 77 and certain other languages do not permit recursion. A few functional languages do not permit iteration. Most modern languages, however, provide both mechanisms. Iteration is in some sense the more “natural” of the two in imperative languages, because it is based on the repeated modification of variables. Recursion is the more natural of the two in functional languages, because it does not change variables. In the final analysis, which to use in which circumstance is mainly a matter of taste. To compute a sum,

110f

1i10f(i)

si1_e

使用迭代似乎很自然。在 C 语言中,有人会说

it seems natural to use iteration. In C one would say

类型定义 int (*int_func) (int);

typedef int (*int_func) (int);

int 总和(int_func f, int 低, int 高){

int summation(int_func f, int low, int high) {

 /* 假设低 <= 高 */

 /* assume low <= high */

 int总计=0;

 int total = 0;

 int 我;

 int i;

 对于(i = 低;i <= 高;i++){

 for (i = low; i <= high; i++) {

  total += f(i); //(C 将自动取消引用)

  total += f(i); // (C will automatically dereference

   // 当我们尝试调用它时,它是一个函数指针。)

   // a function pointer when we attempt to call it.)

 }

 }

 返回总数;

 return total;

}

}

例 6.81

Example 6.81

一个“自然递归”的问题

A “naturally recursive” problem

要计算由递归定义的值,

To compute a value defined by a recurrence,

最大公约数一个b正整数一个b{一个如果一个=b最大公约数一个bb如果一个>b最大公约数一个b一个如果b=一个

gcd(a,b)(positive integers,a,b){aifa=bgcd(ab,b)ifa>bgcd(a,ba)ifb=a

si2_e

递归看起来更自然:

recursion may seem more natural:

int gcd(int a,int b){

int gcd(int a, int b) {

 /* 假设 a, b > 0 */

 /* assume a, b > 0 */

 如果 (a == b) 返回 a;

 if (a == b) return a;

 否则如果 (a > b) 返回 gcd(ab, b);

 else if (a > b) return gcd(a-b, b);

 否则返回 gcd(a,ba);

 else return gcd(a, b-a);

}

}

例 6.82

Example 6.82

用另一种方式解决问题

Implementing problems “the other way”

在这两种情况下,选择都可能朝着相反的方向:

In both these cases, the choice could go the other way:

类型定义 int (*int_func) (int);

typedef int (*int_func) (int);

int 总和(int_func f, int 低, int 高){

int summation(int_func f, int low, int high) {

 /* 假设低 <= 高 */

 /* assume low <= high */

 如果 (low == high) 返回 f(low);

 if (low == high) return f(low);

 否则返回 f(low) + 总和(f, low+1, high);

 else return f(low) + summation(f, low+1, high);

}

}

int gcd(int a,int b){

int gcd(int a, int b) {

 /* 假设 a, b > 0 */

 /* assume a, b > 0 */

 当 (a != b) {

 while (a != b) {

  如果(a > b)a = ab;

  if (a > b) a = a-b;

  否则b = ba;

  else b = b-a;

 }

 }

 返回一个;

 return a;

}

}

尾递归

Tail Recursion

例 6.83

Example 6.83

尾递归的迭代实现

Iterative implementation of tail recursion

有时人们会争论迭代比递归更有效。更准确的说法是,迭代的简单实现通常比递归的简单实现更有效。在上面的例子中,如果递归实现进行真正的子例程调用,在运行时堆栈上为局部变量和簿记信息分配空间,则求和和最大除数的迭代实现将比递归实现更有效。然而,“优化”编译器,特别是为函数式语言设计的编译器,通常能够为递归函数生成出色的代码。对于尾递归函数(如上面的gcd) ,它尤其有可能这样做。尾递归函数是递归调用后从不进行额外计算的函数:返回值只是递归调用返回的值。对于这样的函数,动态分配的堆栈空间是不必要的:编译器在进行递归调用时可以重用属于当前迭代的空间。实际上,一个好的编译器会将上面的递归gcd函数重新构造如下:

It is sometimes argued that iteration is more efficient than recursion. It is more accurate to say that naive implementation of iteration is usually more efficient than naive implementation of recursion. In the examples above, the iterative implementations of summation and greatest divisors will be more efficient than the recursive implementations if the latter make real subroutine calls that allocate space on a run-time stack for local variables and bookkeeping information. An “optimizing” compiler, however, particularly one designed for a functional language, will often be able to generate excellent code for recursive functions. It is particularly likely to do so for tail-recursive functions such as gcd above. A tail-recursive function is one in which additional computation never follows a recursive call: the return value is simply whatever the recursive call returns. For such functions, dynamically allocated stack space is unnecessary: the compiler can reuse the space belonging to the current iteration when it makes the recursive call. In effect, a good compiler will recast the recursive gcd function above as follows:

int gcd(int a,int b){

int gcd(int a, int b) {

 /* 假设 a, b > 0 */

 /* assume a, b > 0 */

开始:

start:

 如果 (a == b) 返回 a;

 if (a == b) return a;

 否则,如果(a > b){

 else if (a > b) {

  a = ab; 转到开始;

  a = a-b; goto start;

 } 别的 {

 } else {

  b = ba; 转到开始;

  b = b-a; goto start;

 }

 }

}

}

即使对于非尾递归函数,自动的、通常简单的转换也会产生尾递归代码。转换的一般情况是采用所谓的延续传递风格[ FWH017-8]。实际上,递归函数总是可以在从递归调用返回后避免执行任何工作,方法是将该工作以延续的形式传递到递归调用中。

Even for functions that are not tail-recursive, automatic, often simple transformations can produce tail-recursive code. The general case of the transformation employs conversion to what is known as continuation-passing style [FWH01, Chaps. 78]. In effect, a recursive function can always avoid doing any work after returning from a recursive call by passing that work into the recursive call, in the form of a continuation.

例 6.84

Example 6.84

手动创建尾递归代码

By-hand creation of tail-recursive code

一些特定的转换(不基于连续传递)经常被函数式语言的熟练用户使用。例如,考虑上面的递归求和函数,用 Scheme 编写如下:

Some specific transformations (not based on continuation passing) are often employed by skilled users of functional languages. Consider, for example, the recursive summation function above, written here in Scheme:

(定义求和

(define summation

 (lambda(流量高)

 (lambda (f low high)

 (如果(=低高)

 (if (= low high)

  (流动);然后部分

  (f low) ; then part

  (+ (f 流) (总和 f (+ 低 1) 高))))) ; 否则部分

  (+ (f low) (summation f (+ low 1) high))))) ; else part

回想一下, Scheme 与所有 Lisp 方言一样,使用剑桥波兰表示法来表示表达式。lambda关键字用于引入函数。当递归调用返回时,我们的代码从“右到左”计算总和:从高到低如果程序员(或编译器)认识到加法是结合的,我们可以用尾递归形式重写代码:

Recall that Scheme, like all Lisp dialects, uses Cambridge Polish notation for expressions. The lambda keyword is used to introduce a function. As recursive calls return, our code calculates the sum from “right to left”: from high down to low. If the programmer (or compiler) recognizes that addition is associative, we can rewrite the code in a tail-recursive form:

(定义求和

(define summation

 (lambda(流量高小计)

 (lambda (f low high subtotal)

 (如果(=低高)

 (if (= low high)

  (+ 小计(流量))

  (+ subtotal (f low))

  (总和 f (+ 低 1) 高 (+ 小计 (f 流量))))))

  (summation f (+ low 1) high (+ subtotal (f low))))))

此处,subtotal 参数从左到右累加总和,并将其传递给递归调用。由于它是尾递归的,因此可以将此函数转换为不为递归调用分配堆栈空间的机器代码。当然,程序员不想将显式 subtotal 参数传递给初始调用,因此我们将其(参数)隐藏在辅助“帮助”函数中:

Here the subtotal parameter accumulates the sum from left to right, passing it into the recursive calls. Because it is tail recursive, this function can be translated into machine code that does not allocate stack space for recursive calls. Of course, the programmer won't want to pass an explicit subtotal parameter to the initial call, so we hide it (the parameter) in an auxiliary, “helper” function:

(定义求和

(define summation

 (lambda(流量高)

 (lambda (f low high)

 (letrec((sum-helper

 (letrec ((sum-helper

  (lambda(低小计)

  (lambda (low subtotal)

   (让((new_subtotal(+ subtotal(f flow))))

   (let ((new_subtotal (+ subtotal (f low))))

    (如果(=低高)

    (if (= low high)

     新小计

     new_subtotal

     (总和助手(+低1)new_subtotal))))))

     (sum-helper (+ low 1) new_subtotal))))))

  (总和助手低 0))))

  (sum-helper low 0))))

Scheme 中的 let 结构用于引入嵌套作用域,可以在其中定义本地名称(例如new_subtotal )。 letrec结构允许定义递归函数(例如sum-helper)。■

The let construct in Scheme serves to introduce a nested scope in which local names (e.g., new_subtotal) can be defined. The letrec construct permits the definition of recursive functions (e.g., sum-helper). ■

递归思考

Thinking Recursively

例 6.85

Example 6.85

朴素递归斐波那契函数

Naive recursive Fibonacci function

函数式编程的反对者有时会错误地认为递归会导致算法较差的程序。例如,斐波那契数由数学递归定义

Detractors of functional programming sometimes argue, incorrectly, that recursion leads to algorithmically inferior programs. Fibonacci numbers, for example, are defined by the mathematical recurrence

Fn非负整数n{1如果n=0或者n=1Fn1+Fn2否则

Fn(non-negative integern){1ifn=0orn=1Fn1+Fn2otherwise

si3_e

在 Scheme 中实现这种递归的简单方法是

The naive way to implement this recurrence in Scheme is

(定义 fib

(define fib

 (λ(n)

 (lambda (n)

  (条件((= n 0)1)

  (cond ((= n 0) 1)

   ((= n 1)1)

   ((= n 1) 1)

   (#t (+ (fib (- n 1)) (fib (- n 2))))))

   (#t (+ (fib (- n 1)) (fib (- n 2)))))))

   ;#t 在 Scheme 中表示“true”

   ; #t means 'true' in Scheme

例 6.86

Example 6.86

线性迭代斐波那契函数

Linear iterative Fibonacci function

不幸的是,这个算法需要指数时间,而线性时间是可能的。9C 语言中,可以这样写

Unfortunately, this algorithm takes exponential time, when linear time is possible.9 In C, one might write

int fib(int n) {

int fib(int n) {

 int f1 = 1; int f2 = 1;

 int f1 = 1; int f2 = 1;

 int 我;

 int i;

 对于 (i = 2; i <= n; i++) {

 for (i = 2; i <= n; i++) {

  int 温度 = f1 + f2;

  int temp = f1 + f2;

  f1 = f2; f2 = 温度;

  f1 = f2; f2 = temp;

 }

 }

 返回 f2;

 return f2;

}

}

例 6.87

Example 6.87

高效的尾递归斐波那契函数

Efficient tail-recursive Fibonacci function

可以用 Scheme 编写此迭代算法:该语言包含(非函数式)迭代功能。不过,更好的办法可能是从上述求和示例的尾递归版本中汲取灵感,并编写以下O ( n ) 递归函数:

One can write this iterative algorithm in Scheme: the language includes (nonfunctional) iterative features. It is probably better, however, to draw inspiration from the tail-recursive version of the summation example above, and write the following O(n) recursive function:

(定义 fib

(define fib

 (λ(n)

 (lambda (n)

  (letrec((fib-helper

  (letrec ((fib-helper

    (λ(f1 f2 i)

    (lambda (f1 f2 i)

     (如果(= 在)

     (if (= i n)

      f2

      f2

      (fib 辅助 f2 (+ f1 f2) (+ i 1))))))

      (fib-helper f2 (+ f1 f2) (+ i 1))))))

  (fib 帮助器 0 1 0))))

  (fib-helper 0 1 0))))

对于习惯于以函数式风格编写代码的程序员来说,这段代码非常自然。有人可能会说它不是“真正”递归的;它只是将迭代算法转换为尾递归形式,这种说法有一定道理。然而,尽管算法相似,但 C 中的迭代算法和 Scheme 中的尾递归算法之间存在一个重要区别:后者没有副作用。fib -helper函数的每次递归调用都会创建一个新的作用域,其中包含新变量。语言实现可能能够重用同一作用域的先前实例所占用的空间,但它保证这种优化永远不会引入错误。■

For a programmer accustomed to writing in a functional style, this code is perfectly natural. One might argue that it isn't “really” recursive; it simply casts an iterative algorithm in a tail-recursive form, and this argument has some merit. Despite the algorithmic similarity, however, there is an important difference between the iterative algorithm in C and the tail-recursive algorithm in Scheme: the latter has no side effects. Each recursive call of the fib-helper function creates a new scope, containing new variables. The language implementation may be able to reuse the space occupied by previous instances of the same scope, but it guarantees that this optimization will never introduce bugs. ■

6.6.2 应用阶和正态阶求值

6.6.2 Applicative- and Normal-Order Evaluation

到目前为止的讨论中,我们都隐含地假设参数在传递给子程序之前会进行求值。但事实并非如此。可以将未求值的参数表示传递给子程序,并且仅在实际需要该值时才对其进行求值。前一种选择(在调用之前求值)称为应用顺序求值;后一种选择(仅在实际需要该值时求值)称为正常顺序求值。正常顺序求值是宏中自然出现的情况(第 3.7 节)。它也出现在短路布尔求值(第 6.1.5 节)、按名称调用参数(将在第 9.3.1 节中讨论)和某些函数式语言(将在第 11.5 节中讨论)中。

Throughout the discussion so far we have assumed implicitly that arguments are evaluated before passing them to a subroutine. This need not be the case. It is possible to pass a representation of the unevaluated arguments to the subroutine instead, and to evaluate them only when (if) the value is actually needed. The former option (evaluating before the call) is known as applicative-order evaluation; the latter (evaluating only when the value is actually needed) is known as normal-order evaluation. Normal-order evaluation is what naturally occurs in macros (Section 3.7). It also occurs in short-circuit Boolean evaluation (Section 6.1.5), call-by-name parameters (to be discussed in Section 9.3.1), and certain functional languages (to be discussed in Section 11.5).

Algol 60 默认对用户定义函数使用正常顺序求值(应用顺序也可用)。这种选择大概是为了模仿宏的行为(第 3.7 节)。1960 年的大多数程序员主要用汇编语言编写,并且习惯于使用宏功能。由于 Algol 60 的参数传递机制是语言的一部分,而不是文本缩写,因此不会出现优先级误解或命名冲突等问题。然而,副作用仍然是一个大问题。我们将在第9.3.1 节中更详细地讨论 Algol 60 参数。

Algol 60 uses normal-order evaluation by default for user-defined functions (applicative order is also available). This choice was presumably made to mimic the behavior of macros (Section 3.7). Most programmers in 1960 wrote mainly in assembler, and were accustomed to macro facilities. Because the parameter-passing mechanisms of Algol 60 are part of the language, rather than textual abbreviations, problems like misinterpreted precedence or naming conflicts do not arise. Side effects, however, are still very much an issue. We will discuss Algol 60 parameters in more detail in Section 9.3.1.

惰性求值

Lazy Evaluation

从清晰度和效率的角度来看,应用顺序求值通常比正常顺序求值更可取。因此,在大多数语言中采用它是很自然的。然而,在某些情况下,正常顺序求值实际上可以产生更快的代码,或者在应用顺序求值会导致运行时错误时产生有效的代码。在这两种情况下,重要的是,如果参数的值实际上从未需要,那么正常顺序求值有时根本不会求值参数。Scheme 提供了可选的正常顺序以名为delayforce的内置函数形式进行求值。10这些函数提供了惰性求值的实现在没有副作用的情况下,惰性求值具有与正常顺序求值相同的语义,但实现会跟踪哪些表达式已被求值,因此如果在给定的引用环境中需要多次使用这些值,它可以重用它们的值。

From the points of view of clarity and efficiency, applicative-order evaluation is generally preferable to normal-order evaluation. It is therefore natural for it to be employed in most languages. In some circumstances, however, normal-order evaluation can actually lead to faster code, or to code that works when applicative-order evaluation would lead to a run-time error. In both cases, what matters is that normal-order evaluation will sometimes not evaluate an argument at all, if its value is never actually needed. Scheme provides for optional normal-order evaluation in the form of built-in functions called delay and force.10 These functions provide an implementation of lazy evaluation. In the absence of side effects, lazy evaluation has the same semantics as normal-order evaluation, but the implementation keeps track of which expressions have already been evaluated, so it can reuse their values if they are needed more than once in a given referencing environment.

设计与实现

Design & Implementation

6.10 正态阶求值

6.10 Normal-order evaluation

正常顺序求值是我们看到的许多例子之一,在这些例子中,语言设计者由于担心实现成本而放弃了可以说是理想的语义。本章中的其他例子包括副作用自由(允许通过惰性求值实现正常顺序)、迭代器(第 6.5.3 节)和不确定性(第 6.7 节)。然而,如边栏 6.2 中所述,随着时间的推移,人们倾向于牺牲一点速度来换取更清晰的语义和更高的可靠性。在函数式编程社区中,Haskell 及其前身 Miranda 完全没有副作用,并且对所有参数都使用正常顺序(惰性)求值。

Normal-order evaluation is one of many examples we have seen where arguably desirable semantics have been dismissed by language designers because of fear of implementation cost. Other examples in this chapter include side-effect freedom (which allows normal order to be implemented via lazy evaluation), iterators (Section 6.5.3), and nondeterminacy (Section 6.7). As noted in Sidebar 6.2, however, there has been a tendency over time to trade a bit of speed for cleaner semantics and increased reliability. Within the functional programming community, Haskell and its predecessor Miranda are entirely side-effect free, and use normal-order (lazy) evaluation for all parameters.

延迟表达式有时称为承诺。用于跟踪哪些承诺已被评估的机制有时称为记忆化。11由于应用顺序评估是 Scheme 中的默认设置,因此程序员必须使用特殊语法不仅要传递未评估的参数,还要使用它。在 Algol 60 中,子程序头指示要以何种方式传递哪些参数;无论哪种情况,调用点和子程序中的参数使用看起来都相同。

A delayed expression is sometimes called a promise. The mechanism used to keep track of which promises have already been evaluated is sometimes called memoization.11 Because applicative-order evaluation is the default in Scheme, the programmer must use special syntax not only to pass an unevaluated argument, but also to use it. In Algol 60, subroutine headers indicate which arguments are to be passed which way; the point of call and the uses of parameters within subroutines look the same in either case.

例 6.88

Example 6.88

无限数据结构的惰性求值

Lazy evaluation of an infinite data structure

惰性求值的一个重要用途是创建所谓的无限惰性数据结构,这些结构会根据需要“充实”。以下示例改编自 Scheme 手册第 5 版 [ KCR + 98,第 28 页],创建了所有自然数的“列表”:

One important use of lazy evaluation is to create so-called infinite or lazy data structures, which are “fleshed out” on demand. The following example, adapted from version 5 of the Scheme manual [KCR+98, p. 28], creates a “list” of all the natural numbers:

(定义自然

(define naturals

 (letrec ((next (lambda (n) (cons n (delay (next (+ n 1)))))))

 (letrec ((next (lambda (n) (cons n (delay (next (+ n 1)))))))

  (下一个 1)))

  (next 1)))

(定义头车)

(define head car)

(定义尾部(lambda(流)(力(cdr 流))))

(define tail (lambda (stream) (force (cdr stream))))

在这里,cons可以粗略地看作是一个连接运算符。Car返回列表的头部;cdr返回除头部之外的所有内容。根据这些定义,我们可以访问任意数量的自然数:

Here cons can be thought of, roughly, as a concatenation operator. Car returns the head of a list; cdr returns everything but the head. Given these definitions, we can access as many natural numbers as we want:

(头部自然)⇒ 1
(头(尾自然))⇒ 2
(头(尾(尾自然)))⇒ 3

列表将只占用我们实际探索过的空间。更复杂的惰性数据结构(例如树)在组合搜索问题中很有用,在这些问题中,聪明的算法可能只探索潜在巨大搜索空间中“有趣”的部分。■

The list will occupy only as much space as we have actually explored. More elaborate lazy data structures (e.g., trees) can be valuable in combinatorial search problems, in which a clever algorithm may explore only the “interesting” parts of a potentially enormous search space. ■

6.7 不确定性

6.7 Nondeterminacy

我们的最后一类控制流是非确定性。非确定性构造是指在替代方案之间(即在控制路径之间)的选择是故意不指定。我们已经看到了表达式求值中的不确定性示例(第 6.1.4 节):在大多数语言中,运算符或子程序参数可以按任何顺序求值。有些语言,尤其是 Algol 68 和各种并发语言,提供了更广泛的非确定性机制,这些机制也涵盖了语句。

Our final category of control flow is nondeterminacy. A nondeterministic construct is one in which the choice between alternatives (i.e., between control paths) is deliberately unspecified. We have already seen examples of nondeterminacy in the evaluation of expressions (Section 6.1.4): in most languages, operator or subroutine arguments may be evaluated in any order. Some languages, notably Algol 68 and various concurrent languages, provide more extensive nondeterministic mechanisms, which cover statements as well.

06-02-9780124104099 更深入地

IN MORE DEPTH

有关非确定性的进一步讨论可以在配套站点上找到。如果没有非确定性构造,则代码片段的作者必须选择某种任意(人为)顺序,其中顺序无关紧要。这样的选择会使构造正式的正确性证明变得更加困难。一些语言设计者还认为这是不优雅的。非确定性最引人注目的用途出现在并发程序中,其中对线程与其对等线程交互的顺序施加任意选择可能会导致整个系统死锁。对于此类程序,可能需要确保非确定性替代方案之间的选择在某种正式意义上是公平的。

Further discussion of nondeterminism can be found on the companion site. Absent a nondeterministic construct, the author of a code fragment in which order does not matter must choose some arbitrary (artificial) order. Such a choice can make it more difficult to construct a formal correctness proof. Some language designers have also argued that it is inelegant. The most compelling uses for nondeterminacy arise in concurrent programs, where imposing an arbitrary choice on the order in which a thread interacts with its peers may cause the system as a whole to deadlock. For such programs one may need to ensure that the choice among nondeterministic alternatives is fair in some formal sense.

06-01-9780124104099检查你的理解

Check Your Understanding

39. 什么是尾递归函数?为什么尾递归很重要?

39. What is a tail-recursive function? Why is tail recursion important?

40.解释表达式的 应用顺序求值和正常顺序求值之间的区别。在什么情况下,每种方式都是可取的?

40. Explain the difference between applicative- and normal-order evaluation of expressions. Under what circumstances is each desirable?

41. 什么是惰性求值?什么是承诺?什么是记忆化?

41. What is lazy evaluation? What are promises? What is memoization?

42. 请给出两个理由说明为什么惰性求值是可取的。

42. Give two reasons why lazy evaluation may be desirable.

43. 说出一种总是以惰性方式求值参数的语言。

43. Name a language in which parameters are always evaluated lazily.

44. 请给出两个原因说明为什么程序员有时希望控制流是不确定的。

44. Give two reasons why a programmer might sometimes want control flow to be nondeterministic.

6.8 总结和结束语

6.8 Summary and Concluding Remarks

在本章中,我们介绍了编程语言中控制流的主要形式:排序、选择、迭代、过程抽象、递归、并发、异常处理和推测以及不确定性。排序指定某些操作要按顺序一个接一个地发生。选择表示在两个或多个控制流选项中进行选择。迭代和递归是重复执行操作的两种方式。递归根据其自身的更简单实例来定义操作;它依赖于过程抽象。迭代重复操作以产生副作用。顺序和迭代是命令式编程的基础。递归是函数式编程的基础。不确定性允许程序员故意不指定控制流的某些方面。我们只是简要地谈到了并发性;这将是第13 章的主题。过程抽象(子程序)是第 9 章的主题。异常处理和推测将在第 9.4 节第 13.4.4节中介绍。

In this chapter we introduced the principal forms of control flow found in programming languages: sequencing, selection, iteration, procedural abstraction, recursion, concurrency, exception handling and speculation, and nondeterminacy. Sequencing specifies that certain operations are to occur in order, one after the other. Selection expresses a choice among two or more control-flow alternatives. Iteration and recursion are the two ways to execute operations repeatedly. Recursion defines an operation in terms of simpler instances of itself; it depends on procedural abstraction. Iteration repeats an operation for its side effect(s). Sequencing and iteration are fundamental to imperative programming. Recursion is fundamental to functional programming. Nondeterminacy allows the programmer to leave certain aspects of control flow deliberately unspecified. We touched on concurrency only briefly; it will be the subject of Chapter 13. Procedural abstractions (subroutines) are the subject of Chapter 9. Exception handling and speculation will be covered in Sections 9.4 and 13.4.4.

在讨论控制流机制之前,我们先讨论了表达式求值。我们考虑了左值和右值之间的区别,以及变量的值模型(其中变量是数据的命名容器)和变量的引用模型(其中变量是对数据对象的引用)之间的区别。我们考虑了表达式中的优先级、结合性和顺序问题。我们研究了短路布尔求值及其通过跳转代码的实现,这既是影响表达式正确性的语义问题(其子部分并不总是定义明确),也是影响评估复杂布尔表达式所需时间的实现问题。

Our survey of control-flow mechanisms was preceded by a discussion of expression evaluation. We considered the distinction between l-values and r-values, and between the value model of variables, in which a variable is a named container for data, and the reference model of variables, in which a variable is a reference to a data object. We considered issues of precedence, associativity, and ordering within expressions. We examined short-circuit Boolean evaluation and its implementation via jump code, both as a semantic issue that affects the correctness of expressions whose subparts are not always well defined, and as an implementation issue that affects the time required to evaluate complex Boolean expressions.

在我们的调查中,我们遇到过许多控制流构造的例子,它们的语法和语义随着时间的推移已经有了很大的发展。一个重要的早期例子是基于goto的控制流的逐步淘汰和对结构化替代方案的共识的出现。虽然便利性和可读性很难量化,但大多数程序员都同意,像 Ada 这样的语言的控制流构造比 Fortran IV 等语言的控制流构造有了显著的改进。Ada 中专门为纠正早期语言中的控制流问题而设计的功能示例包括结构化构造的显式终止符( end ifend loop等); elsif子句; case语句中的标签范围和默认(others)子句;将for 循环索引隐式声明为只读局部变量;显式return语句;多级循环退出语句;以及异常。

In our survey we encountered many examples of control-flow constructs whose syntax and semantics have evolved considerably over time. An important early example was the phasing out of goto-based control flow and the emergence of a consensus on structured alternatives. While convenience and readability are difficult to quantify, most programmers would agree that the control-flow constructs of a language like Ada are a dramatic improvement over those of, say, Fortran IV. Examples of features in Ada that are specifically designed to rectify control-flow problems in earlier languages include explicit terminators (end if, end loop, etc.) for structured constructs; elsif clauses; label ranges and default (others) clauses in case statements; implicit declaration of for loop indices as read-only local variables; explicit return statements; multilevel loop exit statements; and exceptions.

构造的演进受到许多目标的驱动,包括编程的简易性、语义的优雅性、实现的简易性和运行时效率。在某些情况下,这些目标已被证明是互补的。例如,我们已经看到短路求值既可以加快代码速度,又可以(在许多情况下)使语义更清晰。类似地,为枚举控制循环的索引变量引入新的局部作用域既可以避免循环后索引值的语义问题,也可以(在一定程度上)避免潜在溢出的实现问题。

The evolution of constructs has been driven by many goals, including ease of programming, semantic elegance, ease of implementation, and run-time efficiency. In some cases these goals have proved complementary. We have seen for example that short-circuit evaluation leads both to faster code and (in many cases) to cleaner semantics. In a similar vein, the introduction of a new local scope for the index variable of an enumeration-controlled loop avoids both the semantic problem of the value of the index after the loop and (to some extent) the implementation problem of potential overflow.

在其他情况下,语言语义的改进被认为值得以运行时效率为代价。我们在迭代器的开发中看到了这一点:与许多形式的抽象一样,它们在许多情况下增加了适度的运行时成本(例如,与在循环的控制流中明确嵌入枚举集合的实现相比),但在模块化、清晰度和代码重用机会方面却获得了巨大的回报。同样,Java 的开发人员会认为,对于许多应用程序来说,广泛的语义检查、标准格式的数字类型等提供的可移植性和安全性远比速度重要。

In other cases improvements in language semantics have been considered worth a small cost in run-time efficiency. We saw this in the development of iterators: like many forms of abstraction, they add a modest amount of run-time cost in many cases (e.g., in comparison to explicitly embedding the implementation of the enumerated collection in the control flow of the loop), but with a large pay-back in modularity, clarity, and opportunities for code reuse. In a similar vein, the developers of Java would argue that for many applications the portability and safety provided by extensive semantic checking, standard-format numeric types, and so on are far more important than speed.

在一些情况下,编译器技术的进步或设计人员愿意构建更复杂的编译器,使得合并曾经被认为过于昂贵的功能成为可能。Ada案例语句中的标签范围要求编译器准备好使用二分搜索来生成代码。C++ 中的内联函数消除了在小函数的低效率和宏的混乱语义之间做出选择的需要。异常(我们将在第 9.4.3 节中看到)可以以这样一种方式实现,即它们在常见情况下(当它们不发生时)不产生任何成本,但实现起来相当棘手。迭代器、装箱、泛型(第 7.3.1 节)和一等函数同样相当棘手,但在主流命令式语言中越来越多地出现。

In several cases, advances in compiler technology or in the simple willingness of designers to build more complex compilers have made it possible to incorporate features once considered too expensive. Label ranges in Ada case statements require that the compiler be prepared to generate code employing binary search. In-line functions in C++ eliminate the need to choose between the inefficiency of tiny functions and the messy semantics of macros. Exceptions (as we shall see in Section 9.4.3) can be implemented in such a way that they incur no cost in the common case (when they do not occur), but the implementation is quite tricky. Iterators, boxing, generics (Section 7.3.1), and first-class functions are likewise rather tricky, but are increasingly found in mainstream imperative languages.

某些实现技术(例如,重新排列表达式以发现公共子表达式,或在找到可接受的选择后避免在非确定性构造中评估保护)非常重要,足以证明程序员的负担适度(例如,在必要时添加括号以避免溢出或确保数字稳定性,或确保保护中的表达式没有副作用)。其他语义上有用的机制(例如,惰性求值、延续或真正随机的不确定性)通常被认为足够复杂或昂贵,只有在特殊情况下才值得(如果有的话)。

Some implementation techniques (e.g., rearranging expressions to uncover common subexpressions, or avoiding the evaluation of guards in a nondeterministic construct once an acceptable choice has been found) are sufficiently important to justify a modest burden on the programmer (e.g., adding parentheses where necessary to avoid overflow or ensure numeric stability, or ensuring that expressions in guards are side-effect-free). Other semantically useful mechanisms (e.g., lazy evaluation, continuations, or truly random nondeterminacy) are usually considered complex or expensive enough to be worthwhile only in special circumstances (if at all).

在相对原始的语言中,我们通常可以通过编程约定获得一些缺失功能的好处。例如,在 Fortran 的早期方言中,我们可以将goto的使用限制为模仿更现代语言的控制流的模式。在没有短路求值的语言中,我们可以编写嵌套的选择语句。在没有迭代器的语言中,我们可以编写提供等效功能的子例程集。

In comparatively primitive languages, we can often obtain some of the benefits of missing features through programming conventions. In early dialects of Fortran, for example, we can limit the use of gotos to patterns that mimic the control flow of more modern languages. In languages without short-circuit evaluation, we can write nested selection statements. In languages without iterators, we can write sets of subroutines that provide equivalent functionality.

6.9 练习

6.9 Exercises

6.1我们在 6.1.1 节中指出,在大多数编程语言中,大多数二元算术运算符都是左关联的。然而,在6.1.4 节中,我们还指出,大多数编译器可以自由地以任意顺序评估二元运算符的操作数。这些说法是否矛盾?为什么或为什么不矛盾?

6.1 We noted in Section 6.1.1 that most binary arithmetic operators are left-associative in most programming languages. In Section 6.1.4, however, we also noted that most compilers are free to evaluate the operands of a binary operator in either order. Are these statements contradictory? Why or why not?

6.2如图 6.1所示,Fortran 和 Pascal 为一元和二元减法赋予了相同的优先级。这是否会导致某些表达式的求值不直观?为什么会这样?

6.2 As noted in Figure 6.1, Fortran and Pascal give unary and binary minus the same level of precedence. Is this likely to lead to nonintuitive evaluations of certain expressions? Why or why not?

6.3 示例 6.9中,我们描述了 Pascal 程序中的一个常见错误,该错误是由andor的优先级与算术运算符的优先级相当而引起的。说明在基于流的 C++ I/O 中如何出现类似的问题(如第 C-8.7.3 节所述)。(提示:考虑 << 和 >> 的优先级,以及图 6.1中 C 列中它们下方出现的运算符。)

6.3 In Example 6.9 we described a common error in Pascal programs caused by the fact that and and or have precedence comparable to that of the arithmetic operators. Show how a similar problem can arise in the stream-based I/O of C++ (described in Section C-8.7.3). (Hint: Consider the precedence of << and >>, and the operators that appear below them in the C column of Figure 6.1.)

6.4 将下列表达式翻译成后缀和前缀表示法:

6.4 Translate the following expression into postfix and prefix notation:

[b+平方根b×b4×一个×]/2×一个

[b+sqrt(b×b4×a×c)]/(2×a)

si4_e

一元否定需要一个特殊符号吗?

Do you need a special symbol for unary negation?

6.5 在 Lisp 中,大多数算术运算符被定义为接受两个或更多个参数,而不是严格地接受两个参数。因此(* 2 3 4 5)的计算结果为 120,而(- 16 9 4)的计算结果为 3。说明括号对于消除 Lisp 中算术表达式的歧义是必需的(换句话说,给出一个表达式的例子,当删除括号时,其含义不明确)。

第 6.1.1 节中,我们声称前缀或后缀表示法不会出现优先级和结合性问题。重新表述这一说法,以明确隐藏的假设。

6.5 In Lisp, most of the arithmetic operators are defined to take two or more arguments, rather than strictly two. Thus (* 2 3 4 5) evaluates to 120, and (- 16 9 4) evaluates to 3. Show that parentheses are necessary to disambiguate arithmetic expressions in Lisp (in other words, give an example of an expression whose meaning is unclear when parentheses are removed).

In Section 6.1.1 we claimed that issues of precedence and associativity do not arise with prefix or postfix notation. Reword this claim to make explicit the hidden assumption.

6.6 示例 6.33声称“对于某些x 值,(0.1 + x) * 10.01.0 + (x * 10.0)可能会相差多达 25%,即使0.1x 的数量级相同。”验证此说法。(警告:如果您使用的是 x86 处理器,请注意浮点计算(即使是单精度变量)也是在内部以 80 位精度执行的。只有当中间结果存储到内存(精度有限)并再次读回时,才会出现舍入误差。)

6.6 Example 6.33 claims that “For certain values of x, (0.1 + x) * 10.0 and 1.0 + (x * 10.0) can differ by as much as 25%, even when 0.1 and x are of the same magnitude.” Verify this claim. (Warning: If you're using an x86 processor, be aware that floating-point calculations [even on single-precision variables] are performed internally with 80 bits of precision. Roundoff errors will appear only when intermediate results are stored out to memory [with limited precision] and read back in again.)

6.7在 C 语言中 &(&i)是否有效?解释一下。

6.7 Is &(&i) ever valid in C? Explain.

6.8 采用变量引用模型的语言也倾向于采用自动垃圾收集。这不仅仅是巧合吗?解释一下。

6.8 Languages that employ a reference model of variables also tend to employ automatic garbage collection. Is this more than a coincidence? Explain.

6.9 第 6.1.2 节(“正交性”)中,我们注意到 C 使用 = 进行赋值,使用==进行相等性测试。语言设计者指出:“由于在典型的 C 程序中,赋值的频率大约是相等性测试的两倍,因此运算符的长度应为相等性的一半”[ KR88,第 17 页]。您如何看待这一理由?

6.9 In Section 6.1.2 (“Orthogonality”), we noted that C uses = for assignment and == for equality testing. The language designers state: “Since assignment is about twice as frequent as equality testing in typical C programs, it's appropriate that the operator be half as long” [KR88, p. 17]. What do you think of this rationale?

6.10 考虑一种语言实现,我们希望在其中捕获未初始化变量的每次使用。在6.1.3 节中,我们指出,对于每个可能的位模式都代表有效值的类型,必须使用额外的空间来保存已初始化/未初始化标志。在这样的系统中,动态检查可能很昂贵,主要是因为访问标志需要地址计算。我们可以在常见情况下通过让编译器生成代码来自动用不同的标记值初始化每个变量来降低成本。如果我们在某个时候发现变量的值与标记值不同,那么该变量一定已被初始化。如果它的值标记值,我们必须仔细检查该标志。描述初始化标志的合理分配策略,并展示动态检查所需的汇编语言序列(使用和不使用标记值)。

6.10 Consider a language implementation in which we wish to catch every use of an uninitialized variable. In Section 6.1.3 we noted that for types in which every possible bit pattern represents a valid value, extra space must be used to hold an initialized/uninitialized flag. Dynamic checks in such a system can be expensive, largely because of the address calculations needed to access the flags. We can reduce the cost in the common case by having the compiler generate code to automatically initialize every variable with a distinguished sentinel value. If at some point we find that a variable's value is different from the sentinel, then that variable must have been initialized. If its value is the sentinel, we must double-check the flag. Describe a plausible allocation strategy for initialization flags, and show the assembly language sequences that would be required for dynamic checks, with and without the use of sentinels.

6.11 根据以下上下文无关文法编写一个属性文法,该文法累积布尔表达式的跳转代码(带短路)转换为条件的合成属性代码,然后使用此属性生成 if 语句的代码。stmt → if条件then stmt else stmtother_stmt条件c_term |条件c_term c_termc_factor | c_termc_factor c_factor → ident关系ident | (条件) | 非 (条件)关系→ < | <= | = | <> | > | >=您可以假设otherstmt和 ident 节点的 code 属性已经初始化。(有关提示,请参阅 Fischer 等人的编译器书籍 [ FCL10,第 14.1.4 节]。)

 

  

 

 

 

 

6.11 Write an attribute grammar, based on the following context-free grammar, that accumulates jump code for Boolean expressions (with short-circuiting) into a synthesized attribute code of condition, and then uses this attribute to generate code for if statements.

 stmt → if condition then stmt else stmt

  other_stmt

 conditionc_term | condition or c_term

 c_termc_factor | c_term and c_factor

 c_factor → ident relation ident | ( condition ) | not ( condition )

 relation → < | <= | = | <> | > | >=

You may assume that the code attribute has already been initialized for otherstmt and ident nodes. (For hints, see Fischer et al.'s compiler book [FCL10, Sec. 14.1.4].)

6.12 描述一个程序员可能希望避免布尔表达式的短路求值的情况。

6.12 Describe a plausible scenario in which a programmer might wish to avoid short-circuit evaluation of a Boolean expression.

6.13  Algol 60 和 Algol 68 均未对布尔表达式使用短路求值。然而,在这两种语言中,if…then…else结构都可以用作表达式。说明如何使用if…then…else来实现短路求值的效果。

6.13 Neither Algol 60 nor Algol 68 employs short-circuit evaluation for Boolean expressions. In both languages, however, an if… then … else construct can be used as an expression. Show how to use if…then …else to achieve the effect ofshort-circuit evaluation.

6.14 考虑以下 C 语言表达式:a/b > 0 && b/a > 0。当a为零时,计算该表达式的结果是什么?当b为零时,结果又是什么?尝试设计一种语言,保证当ab (但不是两者)为零时,该表达式的计算结果为假,这是否有意义?解释你的答案。

6.14 Consider the following expression in C: a/b > 0 && b/a > 0. What will be the result of evaluating this expression when a is zero? What will be the result when b is zero? Would it make sense to try to design a language in which this expression is guaranteed to evaluate to false when either a or b (but not both) is zero? Explain your answer.

6.15如 第 6.4.2 节所述,当 case 语句中的控制表达式未出现在分支标签中时,不同语言的处理方式有所不同。C 和 Fortran 90 表示该语句无效。Pascal 和 Modula 表示这会导致动态语义错误。Ada 表示标签必须覆盖表达式类型的所有可能值,因此运行时永远不会出现缺失值的问题。这些替代方案之间的权衡是什么?您更喜欢哪一个?为什么?

6.15 As noted in Section 6.4.2, languages vary in how they handle the situation in which the controlling expression in a case statement does not appear among the labels on the arms. C and Fortran 90 say the statement has no effect. Pascal and Modula say it results in a dynamic semantic error. Ada says that the labels must cover all possible values for the type of the expression, so the question of a missing value can never arise at run time. What are the tradeoffs among these alternatives? Which do you prefer? Why?

6.16 示例 6.64中提到的 for 和 while 循环的等价性并不准确。请给出一个它不成立的例子。提示:思考 continue 语句。

6.16 The equivalence of for and while loops, mentioned in Example 6.64, is not precise. Give an example in which it breaks down. Hint: think about the continue statement.

6.17用 C# 或 Ruby 编写与 图 6.5等效的程序。编写第二个版本,执行按序枚举,而不是按预序枚举。

6.17 Write the equivalent of Figure 6.5 in C# or Ruby. Write a second version that performs an in-order enumeration, rather than preorder.

6.18修改 图 6.6的算法,使其执行按序枚举,而不是按预序。

6.18 Revise the algorithm of Figure 6.6 so that it performs an in-order enumeration, rather than preorder.

6.19编写一个 C++ 前序迭代器,为 示例 6.69中的循环提供树节点。您需要知道(或学习)如何在 C++ 中使用指针、引用、内部类和运算符重载。为了(相对)简单起见,您可以假设树节点中的数据始终是 int;这样就无需使用泛型。您可能希望使用 C++ 标准库中的堆栈抽象。

6.19 Write a C++ preorder iterator to supply tree nodes to the loop in Example 6.69. You will need to know (or learn) how to use pointers, references, inner classes, and operator overloading in C++. For the sake of (relative) simplicity, you may assume that the data in a tree node is always an int; this will save you the need to use generics. You may want to use the stack abstraction from the C++ standard library.

6.20 为示例 6.73中使用的tree_iter类型(struct)和ti_create、ti_done、ti_next、ti_valti_delete函数编写代码。

6.20 Write code for the tree_iter type (struct) and the ti_create, ti_done, ti_next, ti_val, and ti_delete functions employed in Example 6.73.

6.21 用 C#、Python 或 Ruby 编写一个迭代器,产生

6.21 Write, in C#, Python, or Ruby, an iterator that yields

(a) 整数 1 .. n的所有排列

(a) all permutations of the integers 1 ..n

(b)从 1 ..  n(0 ≤ kn范围中k 个整数的所有组合。

(b) all combinations of k integers from the range 1 ..n (0 ≤ kn).

您可以使用列表或数组来表示排列和组合。

You may represent your permutations and combinations using either a list or an array.

6.22 使用迭代器构建一个程序,以某种顺序输出所有结构不同的n 个节点的二叉树。如果两棵树的节点数不同,或者它们的左子树或右子树的结构不同,则认为这两棵树的结构不同。例如,有五棵结构不同的三节点树:

6.22 Use iterators to construct a program that outputs (in some order) all structurally distinct binary trees of n nodes. Two trees are considered structurally distinct if they have different numbers of nodes or if their left or right subtrees are structurally distinct. There are, for example, five structurally distinct trees of three nodes:

u06-01-9780124104099

这些最容易以“点括号形式”输出:(((.).).) ((.(.)).) ((.).(.)) (.((.).)) (.(.(.)))(提示:递归思考!如果需要帮助,请参阅Finkel [ Fin96 ]文本第 2.2 节。)

 

 

 

 

 

These are most easily output in “dotted parenthesized form”:

 (((.).).)

 ((.(.)).)

 ((.).(.))

 (.((.).))

 (.(.(.)))

(Hint: Think recursively! If you need help, see Section 2.2 of the text by Finkel [Fin96].)

6.23 使用线程在 Java 中构建真正的迭代器。(这需要了解第 13 章中的材料。)使您的解决方案尽可能简洁和通用。特别是,您应该提供标准的IteratorIEnumerable接口,以便与扩展的 for 循环一起使用,但程序员不必编写这些接口。相反,他或她应该编写一个带有Iterate方法的类,该方法又应该能够调用您也应该提供的Yield方法。评估您的解决方案的成本。它比标准 Java 迭代器对象贵多少?

6.23 Build true iterators in Java using threads. (This requires knowledge of material in Chapter 13.) Make your solution as clean and as general as possible. In particular, you should provide the standard Iterator or IEnumerable interface, for use with extended for loops, but the programmer should not have to write these. Instead, he or she should write a class with an Iterate method, which should in turn be able to call a Yield method, which you should also provide. Evaluate the cost of your solution. How much more expensive is it than standard Java iterator objects?

6.24 在面向表达式的语言(例如 Algol 68 或 Lisp)中,while循环( Lisp 中的do循环)具有表达式值。您认为应该如何确定这个值?(在 Algol 68 和 Lisp 中如何确定?)这个值是面向表达式的无用产物吗?还是有合理的程序可以实际使用它?如果循环中的条件是循环主体永远不会执行,您认为应该发生什么?

6.24 In an expression-oriented language such as Algol 68 or Lisp, a while loop (a do loop in Lisp) has a value as an expression. How do you think this value should be determined? (How is it determined in Algol 68 and Lisp?) Is the value a useless artifact of expression orientation, or are there reasonable programs in which it might actually be used? What do you think should happen if the condition on the loop is such that the body is never executed?

6.25 考虑一个中间测试循环,这里用 C 语言编写,它在输入中查找空行:for (;;) { line = read_line(); if (all_blanks(line)) break; consumer_line(line); }

 

  

 

 

 

说明如果没有中期测试循环,如何使用 while 或 do( repeat )循环完成相同的任务。(提示:一种替代方案重复部分代码;另一种引入布尔标志变量。)这些替代方案与中期测试版本相比如何?

6.25 Consider a mid-test loop, here written in C, that looks for blank lines in its input:

 for (;;) {

  line = read_line();

 if (all_blanks(line)) break;

 consume_line(line);

 }

Show how you might accomplish the same task using a while or do (repeat) loop, if mid-test loops were not available. (Hint: One alternative duplicates part of the code; another introduces a Boolean flag variable.) How do these alternatives compare to the mid-test version?

6.26  Rubin [ Rub87 ] 使用了下面的例子(这里用 C 语言重写)来支持goto语句:int first_zero_row = -1; /* none */ int i, j; for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { if (A[i][j]) goto next; } first_zero_row = i; break; next: ; }该代码的目的是找出一个n × n矩阵的第一个全零行(如果有)。你觉得这个例子有说服力吗?C 语言中有没有好的结构化替代方案?有什么语言吗?

 

 

 

  

   

  

  

  

 

 

6.26 Rubin [Rub87] used the following example (rewritten here in C) to argue in favor of a goto statement:

 int first_zero_row = -1; /* none */

 int i, j;

 for (i = 0; i < n; i++) {

  for (j = 0; j < n; j++) {

   if (A[i][j]) goto next;

  }

  first_zero_row = i;

  break;

 next: ;

 }

The intent of the code is to find the first all-zero row, if any, of an n × n matrix. Do you find the example convincing? Is there a good structured alternative in C? In any language?

6.27  Bentley[ Ben00第 4 章]对二分查找给出了如下非正式描述:

6.27 Bentley [Ben00, Chap. 4] provides the following informal description of binary search:

我们要确定已排序的数组X[1..N]是否包含元素T ...。二分搜索通过跟踪数组中的某个范围来解决这个问题,如果T位于数组中的任何位置,则它必须位于该范围中。最初,范围是整个数组。通过将其中间元素与T进行比较并丢弃一半范围,可以缩小范围。该过程持续进行,直到在数组中发现T或知道它必须位于的范围为空。

We are to determine whether the sorted array X[1..N] contains the element T…. Binary search solves the problem by keeping track of a range within the array in which T must be if it is anywhere in the array. Initially, the range is the entire array. The range is shrunk by comparing its middle element to T and discarding half the range. The process continues until T is discovered in the array or until the range in which it must lie is known to be empty.

用您最喜欢的命令式编程语言编写二分查找代码。您认为哪种循环结构最有用?注意:当他要求一百多名专业程序员解决这个问题时,本特利发现只有大约 10% 的人第一次就答对了,而且没有进行测试。

Write code for binary search in your favorite imperative programming language. What loop construct(s) did you find to be most useful? NB: when he asked more than a hundred professional programmers to solve this problem, Bentley found that only about 10% got it right the first time, without testing.

6.28 循环不变量是每次迭代时在循环体内的给定点保证为真的条件。循环不变量在公理语义中起着重要作用,公理语义是一种用于证明程序属性的形式化推理系统。以一种不太正式的方式,识别(并写下!)循环不变量的程序员更有可能编写正确的代码。显示上一个练习的解决方案的循环不变量。

(提示:您会发现 < 和 ≤ [或 > 和 ≥] 之间的区别至关重要。)

6.28 A loop invariant is a condition that is guaranteed to be true at a given point within the body of a loop on every iteration. Loop invariants play a major role in axiomatic semantics, a formal reasoning system used to prove properties of programs. In a less formal way, programmers who identify (and write down!) the invariants for their loops are more likely to write correct code. Show the loop invariant(s) for your solution to the preceding exercise.

(Hint: You will find the distinction between < and ≤ [or between > and ≥] to be crucial.)

6.29 如果你已经学习过自动机理论或递归函数理论课程,请解释为什么while循环比for循环更强大。(如果你没有学习过这样的课程,请跳过这个问题!)请注意,我们这里指的是 Ada 风格的for循环,而不是 C 风格的 for 循环。

6.29 If you have taken a course in automata theory or recursive function theory, explain why while loops are strictly more powerful than for loops. (If you haven't had such a course, skip this question!) Note that we're referring here to Ada-style for loops, not C-style.

6.30说明如何计算一般 Fortran 90 风格 do循环的迭代次数。您的代码应以类似汇编的符号编写,并应保证适用于所有有效的界限和步长。小心溢出!(提示:虽然循环的界限和步长可以是正数或负数,但您可以安全地使用无符号整数作为迭代计数。)

6.30 Show how to calculate the number of iterations of a general Fortran 90-style do loop. Your code should be written in an assembler-like notation, and should be guaranteed to work for all valid bounds and step sizes. Be careful of overflow! (Hint: While the bounds and step size of the loop can be either positive or negative, you can safely use an unsigned integer for the iteration count.)

6.31 用 Scheme 或 ML 编写一个尾递归函数来计算n 的阶乘。(提示:你可能需要定义一个“辅助”函数,如第 6.6.1 节所述。)n=1n=1×2××nsi5_e

6.31 Write a tail-recursive function in Scheme or ML to compute n factorial (n!=1ini=1×2××n). (Hint: You will probably want to define a “helper” function, as discussed in Section 6.6.1.)

6.32 是否可以编写经典快速排序算法的尾递归版本?为什么或为什么不可以?

6.32 Is it possible to write a tail-recursive version of the classic quicksort algorithm? Why or why not?

6.33 给出一个 C 语言中的例子,其中内联子程序可能比功能等效的宏快得多。给出另一个宏可能更快的例子。(提示:考虑参数的应用顺序与正常顺序的求值。)

6.33 Give an example in C in which an in-line subroutine may be significantly faster than a functionally equivalent macro. Give another example in which the macro is likely to be faster. (Hint: Think about applicative vs normal-order evaluation of arguments.)

6.34 使用惰性求值(delayforce)在 Scheme 中实现迭代器对象。更具体地说,让迭代器为空列表或由一个元素和一个承诺组成的对,当强制时将返回一个迭代器。给出一个返回迭代器的uptoby函数的代码,以及一个接受一个参数函数和一个迭代器作为参数的for -iter函数的代码。这些应该允许您评估这样的表达式(for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))请注意,与标准 Scheme 的for-each 不同,for-iter不应该要求存在一个包含要迭代的元素的列表; (for-iter f (uptoby 1 n 1))所需的内在空间应该只是O (1),而不是O(n)

 

6.34 Use lazy evaluation (delay and force) to implement iterator objects in Scheme. More specifically, let an iterator be either the null list or a pair consisting of an element and a promise which when forced will return an iterator. Give code for an uptoby function that returns an iterator, and a for-iter function that accepts as arguments a one-argument function and an iterator. These should allow you to evaluate such expressions as

 (for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))

Note that unlike the standard Scheme for-each, for-iter should not require the existence of a list containing the elements over which to iterate; the intrinsic space required for (for-iter f (uptoby 1 n 1)) should be only O(1), rather than O(n).

6.35  (困难)使用call-with-current-continuation(call/cc)在 Scheme 中实现以下结构化非本地控制传输。(这需要了解第 11 章中的材料。)您可能希望查阅 Scheme 手册,以获取有关call/cc以及define-syntaxdynamic-wind的文档。

6.35 (Difficult) Use call-with-current-continuation (call/cc) to implement the following structured nonlocal control transfers in Scheme. (This requires knowledge of material in Chapter 11.) You will probably want to consult a Scheme manual for documentation not only on call/cc, but on define-syntax and dynamic-wind as well.

(a) 多级返回。以Common Lisp 的catchthrow为范本来设计你的语法。

(a) Multilevel returns. Model your syntax after the catch and throw of Common Lisp.

(b)真正的迭代器。与 练习 6.34类似,迭代器是一个函数,当调用/cc时,它将返回一个空列表或一对由元素和迭代器组成。与上一个练习一样,你的实现应该支持如下表达式

(b) True iterators. In a style reminiscent of Exercise 6.34, let an iterator be a function which when call/cc-ed will return either a null list or a pair consisting of an element and an iterator. As in that previous exercise, your implementation should support expressions like

 (for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))但是,练习 6.34

中的 uptoby 实现需要使用delayforce,您应该提供一个迭代器宏(Scheme特殊形式)和一个 Yield 函数,使 uptoby 看起来像一个带有嵌入Yield的普通尾递归函数:(define uptoby (iterator (low high step) (letrec ((helper (lambda (next) (if (> next high) '() (begin ; else clause (yield next) (helper (+ next step))))))) (helper low))))

 

  

   

     

      

       

       

   

 (for-iter (lambda (e) (display e) (newline)) (uptoby 10 50 3))

Where the implementation of uptoby in Exercise 6.34 required the use of delay and force, however, you should provide an iterator macro (a Scheme special form) and a yield function that allows uptoby to look like an ordinary tail-recursive function with an embedded yield:

 (define uptoby

  (iterator (low high step)

   (letrec ((helper (lambda (next)

     (if (> next high) '()

      (begin ; else clause

       (yield next)

       (helper (+ next step)))))))

   (helper low))))

06-02-9780124104099 6.36–6.40  更深入。

6.36–6.40  In More Depth.

6.10 探索

6.10 Explorations

6.41 循环展开(在练习 C-5.21 和节 C-17.7.1 中描述)是一种代码转换,它复制循环主体并减少迭代次数,从而减少循环开销并通过重新排序指令增加提高处理器流水线性能的机会。展开传统上由编译器的代码改进阶段实现。然而,如果我们需要在编译器无法胜任的系统上“手动优化”时间关键型代码,则可以在源代码级别实现它。不幸的是,如果我们将循环主体复制k次,则必须处理原始循环迭代次数n可能不是 k 的倍数的可能性用 C 语言编写,设k = 4,我们可以将练习 C-5.21 的主循环从i = 0; do { sum += A[i]; squares += A[i] * A[i]; i++; } while (i < N);转换为i = 0; j = N/4;执行 { sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; sum += A[i]; squares += A[i] * A[i]; i++; } while (--j > 0);执行 { sum += A[i]; squares += A[i] * A[i]; i++; } while (i < N);

 

 

  

 



 

 

  

  

  

  

 

 

  

 

1983 年,卢卡斯影业的汤姆·达夫 (Tom Duff) 意识到,通过在 C 语言中交错使用switch语句和循环,可以“简化”此类代码。 这个结果令人吃惊,但却是完全有效的 C 语言。 编程传说中将此称为“达夫装置”:i = 0; j = (N+3)/4; switch (N%4) { case 0: do{ sum += A[i]; squares += A[i] * A[i]; i++; case 3: sum += A[i]; squares += A[i] * A[i]; i++; case 2: sum += A[i]; squares += A[i] * A[i]; i++; case 1: sum += A[i]; squares += A[i] * A[i]; i++; } while (--j > 0); }达夫怀着“骄傲与厌恶”的心情宣布了他的发现。他指出,“很多人……都说 C 语言最糟糕的特性是 switch 不会在每个 case 标签前自动中断。这段代码在这场争论中形成了某种论据,但我不确定它是赞成还是反对。”你怎么看?以这种方式交错循环和 switch 是否合理?编程语言应该允许这样做吗?自动 fall-through 是否是个好主意?

 

 

  

  

  

  

    

 

6.41 Loop unrolling (described in Exercise C-5.21 and Section C-17.7.1) is a code transformation that replicates the body of a loop and reduces the number of iterations, thereby decreasing loop overhead and increasing opportunities to improve the performance of the processor pipeline by reordering instructions. Unrolling is traditionally implemented by the code improvement phase of a compiler. It can be implemented at source level, however, if we are faced with the prospect of “hand optimizing” time-critical code on a system whose compiler is not up to the task. Unfortunately, if we replicate the body of a loop k times, we must deal with the possibility that the original number of loop iterations, n, may not be a multiple of k. Writing in C, and letting k = 4, we might transform the main loop of Exercise C-5.21 from

 i = 0;

 do {

  sum += A[i]; squares += A[i] * A[i]; i++;

 } while (i < N);

to

 i = 0; j = N/4;

 do {

  sum += A[i]; squares += A[i] * A[i]; i++;

  sum += A[i]; squares += A[i] * A[i]; i++;

  sum += A[i]; squares += A[i] * A[i]; i++;

  sum += A[i]; squares += A[i] * A[i]; i++;

 } while (--j > 0);

 do {

  sum += A[i]; squares += A[i] * A[i]; i++;

 } while (i < N);

In 1983, Tom Duff of Lucasfilm realized that code of this sort can be “simplified” in C by interleaving a switch statement and a loop. The result is rather startling, but perfectly valid C. It's known in programming folklore as “Duff's device”:

 i = 0; j = (N+3)/4;

 switch (N%4) {

  case 0: do{ sum += A[i]; squares += A[i] * A[i]; i++;

  case 3: sum += A[i]; squares += A[i] * A[i]; i++;

  case 2: sum += A[i]; squares += A[i] * A[i]; i++;

  case 1: sum += A[i]; squares += A[i] * A[i]; i++;

    } while (--j > 0);

 }

Duff announced his discovery with “a combination of pride and revulsion.” He noted that “Many people… have said that the worst feature of C is that switches don't break automatically before each case label. This code forms some sort of argument in that debate, but I'm not sure whether it's for or against.” What do you think? Is it reasonable to interleave a loop and a switch in this way? Should a programming language permit it? Is automatic fall-through ever a good idea?

6.42 使用你最喜欢的语言和编译器,研究子程序参数的求值顺序。它们通常是从左到右还是从右到左求值?它们是否曾经以其他顺序求值?(你能确定吗?)编写一个程序,其中顺序会对计算结果产生影响。

6.42 Using your favorite language and compiler, investigate the order of evaluation of subroutine parameters. Are they usually evaluated left-to-right or right-to-left? Are they ever evaluated in the other order? (Can you be sure?) Write a program in which the order makes a difference in the results of the computation.

6.43考虑 Pascal、C、Java、C# 和 Common Lisp 所采用的算术溢出的不同方法,如 第 6.1.4 节所述。推测语言设计目标的差异可能导致设计者采用他们所采用的方法。

6.43 Consider the different approaches to arithmetic overflow adopted by Pascal, C, Java, C#, and Common Lisp, as described in Section 6.1.4. Speculate as to the differences in language design goals that might have caused the designers to adopt the approaches they did.

6.44 进一步了解容器类及其支持的设计模式(结构化编程习惯)。探索 C++、Java 和 C# 标准容器库之间的相似之处和不同之处。您觉得这些库中哪一个最有吸引力?为什么?

6.44 Learn more about container classes and the design patterns (structured programming idioms) they support. Explore the similarities and differences among the standard container libraries of C++, Java, and C#. Which of these libraries do you find the most appealing? Why?

6.45 示例中 6.43 6.72 中我们提出 Ruby proc(传递给函数的块,作为隐式额外参数)“大致”等同于 lambda 表达式。事实证明,Ruby 既有 procs有lambda 表达式,它们几乎相同,但又不完全相同。了解详细信息和它们的开发历史。在什么情况下 proc 和 lambda 的行为会有所不同,为什么?

6.45 In Examples 6.43 and 6.72 we suggested that a Ruby proc (a block, passed to a function as an implicit extra argument) was “roughly” equivalent to a lambda expression. As it turns out, Ruby has both procs and lambda expressions, and they're almost—but not quite—the same. Learn about the details, and the history of their development. In what situations will a proc and a lambda behave differently, and why?

6.46 大型系统最流行的习语之一是所谓的访问者模式。它有几种用途,其中一种类似于示例 6.706.71中的“使用一流函数进行迭代”习语。简而言之,容器类的元素提供一个 accept 方法,该方法期望一个实现访问者接口的对象作为参数。这个接口反过来有一个名为visit 的方法,它期望一个元素类型的参数。要遍历集合,我们在访问者对象的visit方法中实现“循环体”。此对象构成第 3.6.3 节中描述的闭包。visit 所需的任何信息除了“循环索引”元素的标识之外)都可以封装在对象的字段中。集合的迭代器方法将访问者对象传递给每个元素的accept方法。每个元素反过来调用访问者对象的 visit 方法,并将自身作为参数传递。

了解有关访问者模式的更多信息。使用它来实现集合的迭代器 — 例如二叉树的前序、中序和后序遍历。访问者与基于迭代器的等效代码相比如何?它们是否添加了新功能?除了迭代之外,访问者还有哪些用处?

6.46 One of the most popular idioms for large-scale systems is the so-called visitor pattern. It has several uses, one of which resembles the “iterating with first-class functions” idiom of Examples 6.70 and 6.71. Briefly, elements of a container class provide an accept method that expects as argument an object that implements the visitor interface. This interface in turn has a method named visit that expects an argument of element type. To iterate over a collection, we implement the “loop body” in the visit method of a visitor object. This object constitutes a closure of the sort described in Section 3.6.3. Any information that visit needs (beyond the identify of the “loop index” element) can be encapsulated in the object's fields. An iterator method for the collection passes the visitor object to the accept method of each element. Each element in turn calls the visit method of the visitor object, passing itself as argument.

Learn more about the visitor pattern. Use it to implement iterators for a collection—preorder, inorder, and postorder traversals of a binary tree, for example. How do visitors compare with equivalent iterator-based code? Do they add new functionality? What else are visitors good for, in addition to iteration?

06-02-9780124104099 6.47–6.50 更深入。

6.47–6.50 In More Depth.

6.11 书目注释

6.11 Bibliographic Notes

本章讨论的许多问题在有关编程语言历史的论文中都有突出的体现。第1 章的参考书目注释中可以找到几篇这样的论文。在 Feuer 和 Gehani 编辑的论文集 [ FG84 ]中可以找到 15 篇比较 Ada、C 和 Pascal 的论文。各个语言的参考资料可以在附录 A中找到。

Many of the issues discussed in this chapter feature prominently in papers on the history of programming languages. Pointers to several such papers can be found in the Bibliographic Notes for Chapter 1. Fifteen papers comparing Ada, C, and Pascal can be found in the collection edited by Feuer and Gehani [FG84]. References for individual languages can be found in Appendix A.

Niklaus Wirth 在过去 30 年中负责了一系列有影响力的语言,包括 Pascal [ Wir71 ]、其前身 Algol W [ WH66 ] 以及后继者 Modula [ Wir77b ]、Modula-2 [ Wir85b ] 和 Oberon [ Wir88b ]。Algol W 的 case 语句由 Hoare [ Hoa81 ]提出。Bernstein [ Ber85 ] 考虑了 case 的各种替代实现,包括适用于由多个密集的值“簇”组成的标签集的多级版本。Guarded 命令(第 C-6.7 节)由 Dijkstra [ Dij75 ] 提出。Duff 的设备(Exploration 6.41)最初于 1984 年 5 月发布到早期的在线讨论组系统 netnews。原始帖子似乎是丢失了,但达夫对此的评论可以在许多互联网网站上找到,包括www.lysator.liu.se/c/duffs-device.html

Niklaus Wirth has been responsible for a series of influential languages over a 30-year period, including Pascal [Wir71], its predecessor Algol W [WH66], and the successors Modula [Wir77b], Modula-2 [Wir85b], and Oberon [Wir88b]. The case statement of Algol W is due to Hoare [Hoa81]. Bernstein [Ber85] considers a variety of alternative implementations for case, including multilevel versions appropriate for label sets consisting of several dense “clusters” of values. Guarded commands (Section C-6.7) are due to Dijkstra [Dij75]. Duff's device (Exploration 6.41) was originally posted to netnews, an early on-line discussion group system, in May of 1984. The original posting appears to have been lost, but Duff's commentary on it can be found at many Internet sites, including www.lysator.liu.se/c/duffs-device.html.

关于 goto 语句的所谓优点或缺点的争论至少可以追溯到 20 世纪 60 年代初期,但在 Dijkstra 于 1968 年发表的一篇文章(“Go To 语句被认为有害” [ Dij68b ])之后,争论变得更加激烈。20 世纪 70 年代的结构化编程运动以 Dahl、Dijkstra 和 Hoare 的文章命名 [ DDH72 ]。Rubin 在 1987 年发表了一封反对信(“'GOTO 被认为有害' 被认为有害” [ Rub87 ];练习 6.26)引起了一连串的回应。

Debate over the supposed merits or evils of the goto statement dates from at least the early 1960s, but became a good bit more heated in the wake of a 1968 article by Dijkstra (“Go To Statement Considered Harmful” [Dij68b]). The structured programming movement of the 1970s took its name from the text of Dahl, Dijkstra, and Hoare [DDH72]. A dissenting letter by Rubin in 1987 (“'GOTO Considered Harmful' Considered Harmful” [Rub87]; Exercise 6.26) elicited a flurry of responses.

本章中所说的“变量的引用模型”在 Clu 中被称为“对象模型”;Liskov 和 Guttag 在他们关于抽象和规范的文本 [ LG86 ] 的第 2.3和 2.4.2节中对其进行了描述。Liskov 等人的一篇文章 [ LSAS77 ] 以及 Liskov 和 Guttag 文本的第 6 章描述了 Clu 迭代器。Griswold和 Griswold [ GG96 ] 在文本的第 11 章和14 章中讨论了图标生成器。Thomas等人 [ TFH13 ] 在文本的第 4 章中讨论了 Ruby 块、过程和迭代器。练习 6.22中的树枚举算法最初由 Solomon 和 Finkel [ SF80 ]提出(没有迭代器)。

What has been called the “reference model of variables” in this chapter is called the “object model” in Clu; Liskov and Guttag describe it in Sections 2.3 and 2.4.2 of their text on abstraction and specification [LG86]. Clu iterators are described in an article by Liskov et al. [LSAS77], and in Chapter 6 of the Liskov and Guttag text. Icon generators are discussed in Chapters 11 and 14 of the text by Griswold and Griswold [GG96]. Ruby blocks, procs, and iterators are discussed in Chapter 4 of the text by Thomas et al. [TFH13]. The tree-enumeration algorithm of Exercise 6.22 was originally presented (without iterators) by Solomon and Finkel [SF80].

有几篇文章讨论了使用不变量(练习 6.28 )作为编写正确程序的工具。特别值得注意的是 Dijkstra [ Dij76 ] 和 Gries [ Gri81 ]的作品。Kernighan 和 Plauger 对编写优秀程序的艺术进行了更为非正式的讨论 [ KP78 ]。

Several texts discuss the use of invariants (Exercise 6.28) as a tool for writing correct programs. Particularly noteworthy are the works of Dijkstra [Dij76] and Gries [Gri81]. Kernighan and Plauger provide a more informal discussion of the art of writing good programs [KP78].

Blizzard [ SFL + 94 ] 和 Shasta [ SG96 ] 软件分布式共享内存 (S-DSM) 系统利用了哨兵 (练习 6.10 )。我们将在13.2.1 节讨论 S-DSM 。

The Blizzard [SFL+94] and Shasta [SG96] systems for software distributed shared memory (S-DSM) make use of sentinels (Exercise 6.10). We will discuss S-DSM in Section 13.2.1.

Michaelson [ Mic89第 8 章] 提供了应用顺序、正常顺序和惰性求值的通俗易懂的形式化处理。记忆化的概念最初由 Michie [ Mic68 ] 提出。Friedman、Wand 和 Haynes 对延续传递风格进行了精彩的讨论 [ FWH017-8]。

Michaelson [Mic89, Chap. 8] provides an accessible formal treatment of applicative-order, normal-order, and lazy evaluation. The concept of memoization is originally due to Michie [Mic68]. Friedman, Wand, and Haynes provide an excellent discussion of continuation-passing style [FWH01, Chaps. 78].


1前缀表示法由 20 世纪早期的波兰逻辑学家推广;类似 Lisp 的括号语法最早由哈佛大学 (马萨诸塞州剑桥) 的哲学家 WV Quine 使用(用于非计算目的)。

1 Prefix notation was popularized by Polish logicians of the early 20th century; Lisp-like parenthesized syntax was first employed (for noncomputational purposes) by philosopher W. V. Quine of Harvard University (Cambridge, MA).

2大多数作者仅将术语“中缀”用于二元运算符。多字运算符可能被称为“混合固定”,或未命名。

2 Most authors use the term “infix” only for binary operators. Multiword operators may be called “mixfix,” or left unnamed.

3从历史上看,C 缺少单独的布尔类型。C99 添加了 _ Bool,但它实际上只是一个 1 位整数。

3 Historically, C lacked a separate Boolean type. C99 added _Bool, but it's really just a 1 -bit integer.

4此处显示的语法适用于 Perl、Python 和 Ruby。Clu 使用 := 进行赋值。ML 要求每个元组周围都加上括号。

4 The syntax shown here is for Perl, Python, and Ruby. Clu uses := for assignment. ML requires parentheses around each tuple.

5对于间接访问的变量(例如,在使用变量参考模型的语言中),编译器通常可以通过将初始值放在静态内存中并仅在详细说明时创建指向它的指针来降低初始化堆栈或堆变量的成本。

5 For variables that are accessed indirectly (e.g., in languages that employ a reference model of variables), a compiler can often reduce the cost of initializing a stack or heap variable by placing the initial value in static memory, and only creating the pointer to it at elaboration time.

6 Edsger W. Dijkstra (1930–2002) 为我们现代理解并发性奠定了坚实的逻辑基础。除其他贡献外,他还对第 13.3.5 节的信号量以及结构化编程的大量实际开发做出了贡献。他于 1972 年获得 ACM 图灵奖。

6 Edsger W. Dijkstra (1930–2002) developed much of the logical foundation of our modern understanding of concurrency. He was also responsible, among many other contributions, for the semaphores of Section 13.3.5 and for much of the practical development of structured programming. He received the ACM Turing Award in 1972.

7不幸的是,不同语言的术语并不一致。Euclid 使用术语“生成器”来表示此处所谓的“迭代器对象”。Python 在此处将其用于所谓的“真迭代器”。

7 Unfortunately, terminology is not consistent across languages. Euclid uses the term “generator” for what are called “iterator objects” here. Python uses it for what are called “true iterators” here.

8由于迭代器以非常规则的方式与循环交错,因此它们比完全通用的线程更容易(且更便宜)地实现。我们将在C-9.5.3 节中进一步考虑实现选项。

8 Because iterators are interleaved with loops in a very regular way, they can be implemented more easily (and cheaply) than fully general threads. We will consider implementation options further in Section C-9.5.3.

9实际上,使用基于二进制矩阵乘法或连续函数最接近整数舍入的算法,可以比线性时间做得更好,但这些方法存在高常数因子成本或数值精度问题。对于大多数目的,线性时间算法是一个合理的选择。

9 Actually, one can do substantially better than linear time using algorithms based on binary matrix multiplication or closest-integer rounding of continuous functions, but these approaches suffer from high constant-factor costs or problems with numeric precision. For most purposes the linear-time algorithm is a reasonable choice.

10更确切地说,delay 是一种特殊形式,而不是函数。它的参数未经求值就传递给它。

10 More precisely, delay is a special form, rather than a function. Its argument is passed to it unevaluated.

11在函数式编程社区中,术语“惰性求值”通常用于任何拒绝求值不需要的函数参数的实现;这包括正常顺序求值的简单实现和此处描述的记忆机制。

11 Within the functional programming community, the term “lazy evaluation” is often used for any implementation that declines to evaluate unneeded function parameters; this includes both naive implementations of normal-order evaluation and the memoizing mechanism described here.

7

类型系统

Type Systems

大多数编程语言都包含表达式和/或对象的类型概念。1类型有几个重要用途:

Most programming languages include a notion of type for expressions and/or objects.1 Types serve several important purposes:

例 7.1

Example 7.1

利用类型信息的操作

Operations that leverage type information

1. 类型为许多操作提供了隐式上下文,这样程序员不必明确指定该上下文。例如,在 C 语言中,如果ab是整数(int )类型,则表达式a + b将使用整数加法;如果ab是浮点数(doublefloat )类型,则表达式 a + b将使用浮点加法。同样,在 Pascal 中,操作new p (其中p是一个指针)将从堆中分配一个大小适合保存p指向的类型的对象的存储块;程序员不必指定(甚至不知道)这个大小。在 C++、Java 和 C# 中,操作 new my_type()不仅分配(并返回指向)适合my_type类型对象的存储块;它还会自动调用与该类型关联的任何用户定义的初始化(构造函数)函数。■

1. Types provide implicit context for many operations, so that the programmer does not have to specify that context explicitly. In C, for instance, the expression a + b will use integer addition if a and b are of integer (int) type; it will use floating-point addition if a and b are of floating-point (double or float) type. Similarly, the operation new p in Pascal, where p is a pointer, will allocate a block of storage from the heap that is the right size to hold an object of the type pointed to by p; the programmer does not have to specify (or even know) this size. In C++, Java, and C#, the operation new my_type() not only allocates (and returns a pointer to) a block of storage sized for an object of type my_type; it also automatically calls any user-defined initialization (constructor) function that has been associated with that type. ■

例 7.2

Example 7.2

通过类型信息捕获的错误

Errors captured by type information

2. 类型限制了在语义有效的程序中可以执行的操作集。例如,它们阻止程序员添加字符和记录,或取集合的反正切,或将文件作为参数传递给需要整数的子例程。虽然没有类型系统可以保证捕获程序员可能错误地放入程序中的每个无意义的操作,但好的类型系统可以捕获足够多的错误,在实践中非常有价值。■

2. Types limit the set of operations that may be performed in a semantically valid program. They prevent the programmer from adding a character and a record, for example, or from taking the arctangent of a set, or passing a file as a parameter to a subroutine that expects an integer. While no type system can promise to catch every nonsensical operation that a programmer might put into a program by mistake, good type systems catch enough mistakes to be highly valuable in practice. ■

3. 如果在源程序中明确指定类型(许多语言都是如此,但并非所有语言都是如此),它们通常可以使程序更易于阅读和理解。实际上,它们充当了程式化的文档,其正确性由编译器检查。(另一方面,对这种文档的需求有时会使程序更难编写。)

3. If types are specified explicitly in the source program (as they are in many but not all languages), they can often make the program easier to read and understand. In effect, they serve as stylized documentation, whose correctness is checked by the compiler. (On the flip side, the need for this documentation can sometimes make the program harder to write.)

例 7.3

Example 7.3

类型作为“可能别名”信息的来源

Types as a source of “may alias” information

4. 如果在编译时已知类型(因为程序员明确指定了类型,或者因为编译器能够推断出类型),则可以使用它们来推动重要的性能优化。作为一个简单的例子,回想一下第 3.5.1 节中介绍的别名概念,并在边栏 3.7 中进行了讨论。如果程序通过指针执行赋值,则编译器可能能够推断出不相关类型的对象不可能受到影响;即使在赋值之前加载,它们的值也可以安全地保留在寄存器中。■

4. If types are known at compile time (either because the programmer specifies them explicitly or because the compiler is able to infer them), they can be used to drive important performance optimizations. As a simple example, recall the concept of aliases, introduced in Section 3.5.1, and discussed in Sidebar 3.7. If a program performs an assignment through a pointer, the compiler may be able to infer that objects of unrelated types cannot possibly be affected; their values can safely remain in registers, even if loaded prior to the assignment. ■

第 7.1 节 更仔细地研究类型的含义和目的。它给出了一些基本定义,并介绍了多态性和正交性的概念。第 7.2 节更仔细地研究了类型检查;特别是,它考虑了类型等价性(什么时候我们可以说两种类型是相同的?)、类型兼容性(什么时候我们可以在给定的上下文中使用给定类型的值?)和类型推断(我们如何从表达式的组件类型和周围上下文的类型推断出表达式的类型?)。

Section 7.1 looks more closely at the meaning and purpose of types. It presents some basic definitions, and introduces the notions of polymorphism and orthogonality. Section 7.2 takes a closer look at type checking; in particular, it considers type equivalence (when can we say that two types are the same?), type compatibility (when can we use a value of a given type in a given context?), and type inference (how do we deduce the type of an expression from the types of its components and that of the surrounding context?).

作为多态性和复杂推理的一个例子,第 7.2.4 节概述了 ML 的类型系统,它在很大程度上将编译的效率和早期错误报告与解释的便利性和灵活性结合在一起。我们在第 7.3 节继续研究多态性,特别强调泛型,它允许将代码主体显式地参数化为多种类型。最后,在第 7.4 节中,我们考虑比较两个复杂对象是否相等或将一个对象赋值给另一个对象的含义。在第 8 章中,我们将考虑一些最重要的复合类型的句法、语义和语用问题:记录、数组、字符串、集合、指针、列表和文件。

As an example of both polymorphism and sophisticated inference, Section 7.2.4 surveys the type system of ML, which combines, to a large extent, the efficiency and early error reporting of compilation with the convenience and flexibility of interpretation. We continue the study of polymorphism in Section 7.3, with a particular emphasis on generics, which allow a body of code to be parameterized explicitly for multiple types. Finally, in Section 7.4, we consider what it means to compare two complex objects for equality, or to assign one into the other. In Chapter 8 we will consider syntactic, semantic, and pragmatic issues for some of the most important composite types: records, arrays, strings, sets, pointers, lists, and files.

7.1 概述

7.1 Overview

计算机硬件可以以多种不同方式解释内存中的位:指令、地址、字符以及不同长度的整数和浮点数。但是,位本身是无类型的:大多数机器上的硬件不会尝试跟踪哪些解释对应于内存中的哪些位置。汇编语言反映了这种无类型的现象:任何类型的操作都可以应用于任意位置的值。相比之下,高级语言几乎总是将类型与值相关联,以提供上面提到的上下文信息和错误检查。

Computer hardware can interpret bits in memory in several different ways: as instructions, addresses, characters, and integer and floating-point numbers of various lengths. The bits themselves, however, are untyped: the hardware on most machines makes no attempt to keep track of which interpretations correspond to which locations in memory. Assembly languages reflect this lack of typing: operations of any kind can be applied to values in arbitrary locations. High-level languages, by contrast, almost always associate types with values, to provide the contextual information and error checking alluded to above.

通俗地说,类型系统包括 (1) 定义类型并将其与某些语言结构关联的机制,以及 (2) 一组类型等价类型兼容性类型推断的规则。必须具有类型的结构恰恰是具有值或可以引用具有值的对象的结构。这些结构包括命名常量、变量、记录字段、参数,有时还有子程序;文字常量(例如173.14“foo”);以及包含这些内容的更复杂的表达式。类型等价规则确定两个值的类型何时相同。类型兼容性规则确定何时可以在给定上下文中使用给定类型的值。类型推断规则根据表达式组成部分的类型或(有时)表达式的周围环境。在具有多态变量或参数的语言中,区分引用或指针的类型和它所引用的对象的类型可能很重要:给定的名称可能在不同时间引用不同类型的对象。

Informally, a type system consists of (1) a mechanism to define types and associate them with certain language constructs, and (2) a set of rules for type equivalence, type compatibility, and type inference. The constructs that must have types are precisely those that have values, or that can refer to objects that have values. These constructs include named constants, variables, record fields, parameters, and sometimes subroutines; literal constants (e.g., 17, 3.14, “foo”); and more complicated expressions containing these. Type equivalence rules determine when the types of two values are the same. Type compatibility rules determine when a value of a given type can be used in a given context. Type inference rules define the type of an expression based on the types of its constituent parts or (sometimes) the surrounding context. In a language with polymorphic variables or parameters, it may be important to distinguish between the type of a reference or pointer and the type of the object to which it refers: a given name may refer to objects of different types at different times.

在某些语言中,子程序被认为具有类型,但在其他语言中则不具有类型。如果子程序是第一类或第二类值(即,如果它们可以作为参数传递、由函数返回或存储在变量中),则它们需要具有类型。在每种情况下,语言中都有一个构造,其值是动态确定的子程序;类型信息允许语言将可接受的值集限制为提供特定子程序接口的值(即特定数量和类型的参数)。在从不动态创建对子程序的引用的静态作用域语言中(子程序始终是第三类值),编译器始终可以识别名称所引用的子程序,并且可以确保正确调用该例程,而不必采用子程序类型的正式概念。

Subroutines are considered to have types in some languages, but not in others. Subroutines need to have types if they are first- or second-class values (i.e., if they can be passed as parameters, returned by functions, or stored in variables). In each of these cases there is a construct in the language whose value is a dynamically determined subroutine; type information allows the language to limit the set of acceptable values to those that provide a particular subroutine interface (i.e., particular numbers and types of parameters). In a statically scoped language that never creates references to subroutines dynamically (one in which subroutines are always third-class values), the compiler can always identify the subroutine to which a name refers, and can ensure that the routine is called correctly without necessarily employing a formal notion of subroutine types.

类型检查是确保程序遵守语言的类型兼容性规则的过程。违反规则的行为称为类型冲突。如果一种语言以语言实现可以强制执行的方式禁止对任何不支持该操作的对象应用任何操作,则该语言被称为强类型语言。如果一种语言是强类型的并且可以在编译时执行类型检查,则该语言被称为静态类型语言。从最严格的意义上讲,很少有语言是静态类型的。在实践中,这个术语通常适用于大多数类型检查可以在编译时执行,其余类型检查可以在运行时执行的语言。

Type checking is the process of ensuring that a program obeys the language's type compatibility rules. A violation of the rules is known as a type clash. A language is said to be strongly typed if it prohibits, in a way that the language implementation can enforce, the application of any operation to any object that is not intended to support that operation. A language is said to be statically typed if it is strongly typed and type checking can be performed at compile time. In the strictest sense of the term, few languages are statically typed. In practice, the term is often applied to languages in which most type checking can be performed at compile time, and the rest can be performed at run time.

自 20 世纪 70 年代中期以来,大多数新开发的语言都趋向于强类型(尽管不一定是静态类型)。有趣的是,C 语言的每个后续版本都变得更加强类型,尽管仍然存在各种漏洞;这些漏洞包括联合、非转换类型转换、具有可变数量参数的子例程以及指针和数组的互操作性(将在8.5.1 节中讨论)。C 的实现很少在运行时检查任何东西。

Since the mid 1970s, most newly developed languages have tended to be strongly (though not necessarily statically) typed. Interestingly, C has become more strongly typed with each successive version of the language, though various loopholes remain; these include unions, nonconverting type casts, subroutines with variable numbers of parameters, and the interoperability of pointers and arrays (to be discussed in Section 8.5.1). Implementations of C rarely check anything at run time.

设计与实现

Design & Implementation

7.1 系统编程

7.1 Systems programming

反对 C 语言中完全类型安全的标准论点是系统程序需要能够偶尔“破坏”类型。例如,考虑实现动态内存管理(例如mallocfree)的代码。此代码必须在不同时间将相同的字节解释为未分配的空间、元数据或(部分)用户定义的数据结构。类型之间的“法定”转换是不可避免的。然而,这种转换不需要很微妙。很大程度上是为了应对使用 C 语言的经验,C# 的设计者选择只允许在明确标记为不安全的代码块中进行破坏类型系统的操作

The standard argument against complete type safety in C is that systems programs need to be able to “break” types on occasion. Consider, for example, the code that implements dynamic memory management (e.g., malloc and free). This code must interpret the same bytes, at different times, as unallocated space, metadata, or (parts of) user-defined data structures. “By fiat” conversions between types are inescapable. Such conversions need not, however, be subtle. Largely in reaction to experience with C, the designers of C# chose to permit operations that break the type system only within blocks of code that have been explicitly labeled unsafe.

动态(运行时)类型检查可以看作是一种后期绑定,并且往往出现在将其他问题延迟到运行时的语言中。因此,静态类型是性能导向型语言的常态;动态类型在旨在简化编程的语言中更为常见。Lisp 和 Smalltalk 是动态(尽管是强)类型的。大多数脚本语言也是动态类型的;一些(例如 Python 和 Ruby)是强类型的。具有动态作用域的语言通常是动态类型的(或根本没有类型):如果编译器无法识别名称所指的对象,它通常也无法确定对象的类型。

Dynamic (run-time) type checking can be seen as a form of late binding, and tends to be found in languages that delay other issues until run time as well. Static typing is thus the norm in languages intended for performance; dynamic typing is more common in languages intended for ease of programming. Lisp and Smalltalk are dynamically (though strongly) typed. Most scripting languages are also dynamically typed; some (e.g., Python and Ruby) are strongly typed. Languages with dynamic scoping are generally dynamically typed (or not typed at all): if the compiler can't identify the object to which a name refers, it usually can't determine the type of the object either.

7.1.1 “类型”的含义

7.1.1 The Meaning of “Type”

虽然每个程序员至少对“类型”的含义有一个非正式的概念,但这个概念可以用几种不同的方式形式化。其中最流行的三种观点是,我们可以称之为外延观点结构观点和基于抽象的观点。从外延的角度来看,类型只是一组值。如果一个值属于该集合,它就具有给定类型;如果一个对象的值保证在该集合中,它就具有给定类型。从结构的角度来看,类型要么是一小组内置类型(整数、字符、布尔、实数等;也称为原始类型或预定义类型)之一,要么是由将类型构造函数记录数组集合等)应用于一个或多个更简单的类型。(“构造函数”一词的这种用法与面向对象语言的初始化函数无关。它也与 ML 中的术语用法有更微妙的不同。)从基于抽象的角度来看,类型是由一组具有明确定义且相互一致的语义的操作组成的接口。对于程序员和语言设计者来说,类型也可能反映这些观点的混合。

While every programmer has at least an informal notion of what is meant by “type,” that notion can be formalized in several different ways. Three of the most popular are what we might call the denotational, structural, and abstraction-based points of view. From the denotational point of view, a type is simply a set of values. A value has a given type if it belongs to the set; an object has a given type if its value is guaranteed to be in the set. From the structural point of view, a type is either one of a small collection of built-in types (integer, character, Boolean, real, etc.; also called primitive or predefined types), or a composite type created by applying a type constructor (record, array, set, etc.) to one or more simpler types. (This use of the term “constructor” is unrelated to the initialization functions of object-oriented languages. It also differs in a more subtle way from the use of the term in ML.) From the abstraction-based point of view, a type is an interface consisting of a set of operations with well-defined and mutually consistent semantics. For both programmers and language designers, types may also reflect a mixture of these viewpoints.

设计与实现

Design & Implementation

7.2 动态类型

7.2 Dynamic typing

脚本语言的日益流行导致许多知名软件开发人员公开质疑静态类型的价值。他们问道:既然我们无法在编译时检查所有内容,那么检查我们可以检查的内容值得付出多少努力?一般来说,编写类型正确的代码比证明我们已经这样做更容易,而静态类型需要这样的证明。随着类型系统变得越来越复杂(由于面向对象、泛型等),静态类型的复杂性也相应增加。

The growing popularity of scripting languages has led a number of prominent software developers to publicly question the value of static typing. They ask: given that we can't check everything at compile time, how much pain is it worth to check the things we can? As a general rule, it is easier to write type-correct code than to prove that we have done so, and static typing requires such proofs. As type systems become more complex (due to object orientation, generics, etc.), the complexity of static typing increases correspondingly.

任何曾经大量使用 Ada 或 C++ 以及 Python 或 Scheme 编写代码的人都会不禁惊叹,在没有复杂类型声明的情况下,编写代码要容易得多,至少对于中等规模的程序而言是如此。当然,动态检查会产生一些运行时开销,并且可能会延迟发现错误,但与人类生产力的潜在提高相比,这越来越被视为微不足道。一种中间立场,以 ML 系列语言为代表,但越来越多地被其他语言(以有限的形式)采用,它保留了类型必须静态已知的要求,但依靠编译器自动推断类型,而不需要一些(或者——在 ML 的情况下——大多数)显式声明。我们将在第 7.2.3 节中进一步讨论这个主题。静态和动态类型以及推断的作用有望成为未来十年一些最有趣的语言争论之一。

Anyone who has written extensively in Ada or C++ on the one hand, and in Python or Scheme on the other, cannot help but be struck at how much easier it is to write code, at least for modest-sized programs, without complex type declarations. Dynamic checking incurs some run-time overhead, of course, and may delay the discovery of bugs, but this is increasingly seen as insignificant in comparison to the potential increase in human productivity. An intermediate position, epitomized by the ML family of languages but increasingly adopted (in limited form) by others, retains the requirement that types be statically known, but relies on the compiler to infer them automatically, without the need for some (or—in the case of ML—most) explicit declarations. We will discuss this topic more in Section 7.2.3. Static and dynamic typing and the role of inference promise to provide some of the most interesting language debates of the coming decade.

在指称语义(形式化程序含义的几种方法之一)中,一组值称为。类型是域,表达式的含义是来自表示表达式类型的域的值。有些域(例如整数)简单且熟悉,其他则更复杂。数组可以看作是来自其元素为函数的域的值;这些函数中的每一个都将某些有限索引类型(通常是整数的子集)的值映射到其他元素类型的值。事实证明,指称语义可以将类型与程序中的所有内容相关联,甚至是具有副作用的语句。赋值语句的含义是来自更高级函数域的值,其每个元素将一个存储(从名称到表示内存当前内容的值的映射)映射到另一个存储,该存储表示赋值后的内存内容。

In denotational semantics (one of several ways to formalize the meaning of programs), a set of values is known as a domain. Types are domains, and the meaning of an expression is a value from the domain that represents the expression's type. Some domains—the integers, for example—are simple and familiar. Others are more complex. An array can be thought of as a value from a domain whose elements are functions; each of these functions maps values from some finite index type (typically a subset of the integers) to values of some other element type. As it turns out, denotational semantics can associate a type with everything in a program—even statements with side effects. The meaning of an assignment statement is a value from a domain of higher-level functions, each of whose elements maps a store—a mapping from names to values that represents the current contents of memory—to another store, which represents the contents of memory after the assignment.

类型的表示视图的优点之一是,它允许我们在许多情况下用集合上的数学运算来描述用户定义的复合类型(记录、数组等)。我们将在第7.1.4 节“复合类型”下再次提到这些运算。由于类型的表示视图基于数学对象,因此它通常会忽略精度和字长有限等实现问题。这种限制并没有乍一看那么严重:对算术溢出等错误的检查通常是在语言的类型系统之外实现的。它们会导致运行时错误,但这种错误不称为类型冲突。

One of the nice things about the denotational view of types is that it allows us in many cases to describe user-defined composite types (records, arrays, etc.) in terms of mathematical operations on sets. We will allude to these operations again under “Composite Types” in Section 7.1.4. Because it is based on mathematical objects, the denotational view of types usually ignores such implementation issues as limited precision and word length. This limitation is less serious than it might at first appear: Checks for such errors as arithmetic overflow are usually implemented outside of the type system of a language anyway. They result in a run-time error, but this error is not called a type clash.

当程序员定义枚举类型(例如, C 语言中的enum hue {red, green, blue})时,他或她肯定会将此类型视为一组值。对于其他类型的用户定义类型,这种表示观点可能并不那么自然。相反,程序员可能会考虑类型是如何从更简单的类型构建而来的,或者考虑其含义或目的。这些思维方式分别反映了结构化和基于抽象的观点。结构化观点由 Algol W 和 Algol 68 率先提出,是 20 世纪 70 年代和 80 年代设计的许多语言的特征。基于抽象的观点由 Simula-67 和 Smalltalk 率先提出,是现代面向对象语言的特征;它也可以在其他各种语言的模块构造中找到,并且几乎可以在任何语言中作为编程规则采用。我们将在第 8 章中更详细地考虑结构化的观点,并在第 10 章中更详细地考虑基于抽象的观点。

When a programmer defines an enumerated type (e.g., enum hue {red, green, blue} in C), he or she certainly thinks of this type as a set of values. For other varieties of user-defined type, this denotational view may not be as natural. Instead, the programmer may think in terms of the way the type is built from simpler types, or in terms of its meaning or purpose. These ways of thinking reflect the structural and abstraction-based points of view, respectively. The structural point of view was pioneered by Algol W and Algol 68, and is characteristic of many languages designed in the 1970s and 1980s. The abstraction-based point of view was pioneered by Simula-67 and Smalltalk, and is characteristic of modern object-oriented languages; it can also be found in the module constructs of various other languages, and it can be adopted as a matter of programming discipline in almost any language. We will consider the structural point of view in more detail in Chapter 8, and the abstraction-based in Chapter 10.

7.1.2 多态性

7.1.2 Polymorphism

多态性我们在3.5.2 节中简要提到过,它的名字来源于希腊语,意思是“具有多种形式”。它适用于设计用于处理多种类型值的代码(数据结构和子程序)。为了保持正确性,类型通常必须具有某些共同的特征,并且代码不能依赖于任何其他特征。共性通常以两种主要方式之一捕获。在参数多态性中,代码以显式或隐式的方式将一个类型(或一组类型)作为参数。在子类型多态性中,代码设计用于处理某些特定类型T的值,但程序员可以定义其他类型作为T的扩展或细化,代码也可以使用这些子类型。

Polymorphism, which we mentioned briefly in Section 3.5.2, takes its name from the Greek, and means “having multiple forms.” It applies to code—both data structures and subroutines—that is designed to work with values of multiple types. To maintain correctness, the types must generally have certain characteristics in common, and the code must not depend on any other characteristics. The commonality is usually captured in one of two main ways. In parametric polymorphism the code takes a type (or set of types) as a parameter, either explicitly or implicitly. In subtype polymorphism, the code is designed to work with values of some specific type T, but the programmer can define additional types to be extensions or refinements of T, and the code will work with these subtypes as well.

显式参数多态性,也称为泛型(或C++ 中的模板),通常出现在静态类型语言中,通常在编译时实现。隐式版本也可以在编译时实现 - 具体来说,在 ML 系列语言中;更常见的是,它与动态类型配对,并在运行时进行检查。

Explicit parametric polymorphism, also known as generics (or templates in C++), typically appears in statically typed languages, and is usually implemented at compile time. The implicit version can also be implemented at compile time—specifically, in ML-family languages; more commonly, it is paired with dynamic typing, and the checking occurs at run time.

子类型多态性主要出现在面向对象语言中。使用静态类型,处理多种类型所需的大部分工作都可以在编译时执行:主要的运行时成本是方法调用的额外间接级别。大多数设想这种实现的语言,包括 C++、Eiffel、OCaml、Java 和 C#,都为泛型提供了单独的机制,也主要在编译时进行检查。子类型和参数多态性的组合对于容器(集合)类特别有用,例如“T 列表”(List<T> )或“ T堆栈”(Stack<T>),其中T最初未指定,稍后可以实例化为几乎任何类型。

Subtype polymorphism appears primarily in object-oriented languages. With static typing, most of the work required to deal with multiple types can be performed at compile time: the principal run-time cost is an extra level of indirection on method invocations. Most languages that envision such an implementation, including C++, Eiffel, OCaml, Java, and C#, provide a separate mechanism for generics, also checked mainly at compile time. The combination of subtype and parametric polymorphism is particularly useful for container (collection) classes such as “list of T” (List<T>) or “stack of T“ (Stack<T>), where T is initially unspecified, and can be instantiated later as almost any type.

相比之下,包括 Smalltalk、Python 和 Ruby 在内的动态类型面向对象语言通常使用单一机制来实现参数多态性和子类型多态性,并将检查延迟到运行时。Objective-C 中也出现了统一的机制,它在静态类型的基础上提供了动态类型对象。

By contrast, dynamically typed object-oriented languages, including Smalltalk, Python, and Ruby, generally use a single mechanism for both parametric and subtype polymorphism, with checking delayed until run time. A unified mechanism also appears in Objective-C, which provides dynamically typed objects on top of otherwise static typing.

在介绍完 ML 中的类型之后,我们将在第 7.3 节中更详细地讨论参数多态性。子类型多态性将主要推迟到第 10 章(介绍面向对象)和第 14.4.4 节(重点介绍脚本语言中的对象)。

We will consider parametric polymorphism in more detail in Section 7.3, after our coverage of typing in ML. Subtype polymorphism will largely be deferred to Chapter 10, which covers object orientation, and to Section 14.4.4, which focuses on objects in scripting languages.

7.1.3 正交性

7.1.3 Orthogonality

例 7.4

Example 7.4

void(简单)类型

void (trivial) type

第 6.1.2 节中,我们讨论了正交性在表达式、语句和控制流构造的设计中的重要性。在高度正交的语言中,这些特性几乎可以以任何组合使用,并且行为一致。正交性在类型系统设计中同样重要。高度正交的语言往往更容易理解、使用和推理。正式的方式。我们已经注意到,像 Algol 68 和 C 这样的语言通过消除(或至少模糊)语句和表达式之间的区别来增强正交性。为了表征为其副作用而执行且没有有用值的语句,某些语言提供了具有单个值的平凡类型。例如,在 C 和 Algol 68 中,用作过程的子例程通常声明为返回类型 void。在 ML 中,平凡类型称为 unit。如果程序员希望调用确实返回值的子例程,但在这种特殊情况下不需要该值(重要的是副作用),那么可以通过将 C 中的返回值“强制转换”为 void 来丢弃它:

In Section 6.1.2 we discussed the importance of orthogonality in the design of expressions, statements, and control-flow constructs. In a highly orthogonal language, these features can be used, with consistent behavior, in almost any combination. Orthogonality is equally important in type system design. A highly orthogonal language tends to be easier to understand, to use, and to reason about in a formal way. We have noted that languages like Algol 68 and C enhance orthogonality by eliminating (or at least blurring) the distinction between statements and expressions. To characterize a statement that is executed for its side effect(s), and that has no useful values, some languages provide a trivial type with a single value. In C and Algol 68, for example, a subroutine that is meant to be used as a procedure is generally declared with a return type of void. In ML, the trivial type is called unit. If the programmer wishes to call a subroutine that does return a value, but the value is not needed in this particular case (all that matters is the side effect[s]), then the return value in C can be discarded by “casting” it to void:

foo_index = 插入符号表 (foo);

foo_index = insert_in_symbol_table(foo);

(void) insert_in_symbol_table(bar); /* 不管它去了哪里 */

(void) insert_in_symbol_table(bar); /* don't care where it went */

 /* 强制类型转换是可选的;如果省略则隐含 */

 /* cast is optional; implied if omitted */

例 7.5

Example 7.5

不使用void进行操作

Making do without void

在没有简单类型的语言(例如 Pascal)中,这两个调用中的后者需要使用虚拟变量:

In a language (e.g., Pascal) without a trivial type, the latter of these two calls would need to use a dummy variable:

var dummy :符号表索引;

var dummy : symbol_table_index;

虚拟 := 插入符号表 (bar);

dummy := insert_in_symbol_table(bar);

作为正交性的另一个例子,考虑“擦除”变量值的常见需求——以表明它不包含其类型的有效值。对于指针类型,我们通常可以使用值null。对于枚举,我们可以在可能值集合中添加一个额外的“以上都不是”替代方案。但这两种技术非常不同,它们不能推广到已经利用了底层实现中所有可用位模式的类型。

As another example of orthogonality, consider the common need to “erase” the value of a variable—to indicate that it does not hold a valid value of its type. For pointer types, we can often use the value null. For enumerations, we can add an extra “none of the above” alternative to the set of possible values. But these two techniques are very different, and they don't generalize to types that already make use of all available bit patterns in the underlying implementation.

例 7.6

Example 7.6

OCaml 中的选项类型

Option types in OCaml

为了以更正交的方式满足“以上皆非”的需求,许多函数式语言(以及一些命令式语言)提供了一种特殊的类型构造函数,通常称为Option 或 Maybe。在 OCaml 中,我们可以这样写

To address the need for “none of the above” in a more orthogonal way, many functional languages—and some imperative languages as well—provide a special type constructor, often called Option or Maybe. In OCaml, we can write

让除以 nd:浮点选项 = (* n 和 d 是参数 *)

let divide n d : float option = (* n and d are parameters *)

 匹配 d 与 (*“float option” 是返回类型 *)

 match d with (* “float option” is the return type *)

 | 0. -> 无

 | 0. -> None

 | _ -> Some (n /. d);; (* 下划线表示“其他任何东西” *)

 | _ -> Some (n /. d);; (* underscore means “anything else” *)

让显示 v : 字符串 =

let show v : string =

 将 v 与

 match v with

 | 无 -> “??”

 | None -> “??”

 | 一些 x -> string_of_float x;;

 | Some x -> string_of_float x;;

这里,如果要求除以零,函数 divide 将返回 None ;否则返回Some x,其中x是所需的商。函数 show 返回“ ?? ”或x的字符串表示形式,具体取决于参数vNone还是Some x。■

Here function divide returns None if asked to divide by zero; otherwise it returns Some x, where x is the desired quotient. Function show returns either “??“ or the string representation of x, depending on whether parameter v is None or Some x. ■

例 7.7

Example 7.7

Swift 中的选项类型

Option types in Swift

选项类型出现在多种其他语言中,包括 Haskell(将其称为Maybe)、Scala、C#、Swift 以及(作为通用库类)Java 和 C++。为了简洁起见,C# 和 Swift 使用尾随问号代替选项构造函数。以下是使用 Swift 重写的上一个示例:

Option types appear in a variety of other languages, including Haskell (which calls them Maybe), Scala, C#, Swift, and (as generic library classes) Java and C++. In the interest of brevity, C# and Swift use a trailing question mark instead of the option constructor. Here is the previous example, rewritten in Swift:

函数除法(n:Double,d:Double)->Double?{

func divide(n : Double, d : Double) -> Double? {

 如果 d == 0 { 返回 nil }

 if d == 0 { return nil }

 返回 n / d

 return n / d

}

}

func show(v : Double?) -> String {

func show(v : Double?) -> String {

 如果 v == nil { 返回 “??” }

 if v == nil { return “??” }

 返回 “\(v!)” // 将 v 插入字符串

 return “\(v!)” // interpolate v into string

}

}

有了这些定义,show(divide(3.0, 4.0)) 的计算结果将为“0.75”,而show(divide(3.0, 0.0)) 的计算结果将为“ ?? ”。■

With these definitions, show(divide(3.0, 4.0)) will evaluate to “0.75”, while show(divide(3.0, 0.0)) will evaluate to “??“. ■

另一个正交性的例子是为复合类型的对象指定字面值。这种字面值有时被称为聚合。它们对于静态数据结构的初始化特别有用;如果没有它们,程序可能需要在运行时浪费时间进行初始化。

Yet another example of orthogonality arises when specifying literal values for objects of composite type. Such literals are sometimes known as aggregates. They are particularly valuable for the initialization of static data structures; without them, a program may need to waste time performing initialization at run time.

例 7.8

Example 7.8

Ada 中的聚合

Aggregates in Ada

Ada 为其所有结构化类型提供聚合。给出以下声明

Ada provides aggregates for all its structured types. Given the following declarations

类型人是记录

type person is record

  名称:字符串(1..10);

  name : string (1..10);

  年龄:整数;

  age : integer;

 结束记录;

 end record;

p, q :人;

p, q : person;

A, B :整数数组(1..10);

A, B : array (1..10) of integer;

我们可以写以下作业:

we can write the following assignments:

p:=(“无名氏”,37);

p := (“Jane Doe “, 37);

q := (年龄 => 36, 姓名 => “John Doe “);

q := (age => 36, name => “John Doe “);

答:= (1, 0, 3, 0, 3, 0, 3, 0, 0, 0);

A := (1, 0, 3, 0, 3, 0, 3, 0, 0, 0);

B := (1 => 1, 3 | 5 | 7 => 3, 其他 => 0);

B := (1 => 1, 3 | 5 | 7 => 3, others => 0);

这里,分配给pA 的聚合是位置相关的;分配给qB 的聚合明确命名了它们的元素。B 的聚合使用简写符号将相同的值 ( 3 ) 分配给数组元素357,并将0分配给所有未命名字段。包括 C、C++、Fortran 90 和 Lisp 在内的几种语言都提供了类似的功能。■

Here the aggregates assigned into p and A are positional; the aggregates assigned into q and B name their elements explicitly. The aggregate for B uses a shorthand notation to assign the same value (3) into array elements 3, 5, and 7, and to assign a 0 into all unnamed fields. Several languages, including C, C++, Fortran 90, and Lisp, provide similar capabilities. ■

ML 提供了一种非常通用的复合表达式工具,它基于构造函数的使用(在11.4.3 节中讨论)。Lambda 表达式(我们在3.6.4 节中看到过,并将在第 11 章中再次讨论)相当于函数值的聚合。

ML provides a very general facility for composite expressions, based on the use of constructors (discussed in Section 11.4.3). Lambda expressions, which we saw in Section 3.6.4 and will discuss again in Chapter 11, amount to aggregates for values that are functions.

7.1.4 类型分类

7.1.4 Classification of Types

不同语言中类型的术语有所不同。本小节介绍最常见术语的定义。大多数语言都提供内置类型,类似于大多数处理器硬件支持的类型:整数、字符、布尔值和实数(浮点数)。

The terminology for types varies some from one language to another. This subsection presents definitions for the most common terms. Most languages provide built-in types similar to those supported in hardware by most processors: integers, characters, Booleans, and real (floating-point) numbers.

布尔值 (有时称为逻辑值) 通常实现为单字节量,其中1代表0代表。在一些语言和实现中,布尔值可能打包成数组,每个值只使用一位。如第 6.1.2 节(“正交性”) 所述,C 在历史上不寻常地省略了布尔类型:大多数语言都期望布尔值,而 C 期望整数,使用零表示,其他任何值表示。C99 引入了一种新的 _Bool类型,但它实际上是一个整数,编译器被允许将其存储在单个位中。如第 C-6.5.4 节所述,Icon 用更通用的成功失败概念取代了布尔值。

Booleans (sometimes called logicals) are typically implemented as single-byte quantities, with 1 representing true and 0 representing false. In a few languages and implementations, Booleans maybe packed into arrays using only one bit per value. As noted in Section 6.1.2 (“Orthogonality”), C was historically unusual in omitting a Boolean type: where most languages would expect a Boolean value, C expected an integer, using zero for false and anything else for true. C99 introduced a new _Bool type, but it is effectively an integer that the compiler is permitted to store in a single bit. As noted in Section C-6.5.4, Icon replaces Booleans with a more general notion of success and failure.

传统上,字符也是用单字节来实现的,通常(但不总是)使用 ASCII 编码。较新的语言(例如 Java 和 C#)使用双字节表示法来适应 Unicode 字符集(的常用部分)。Unicode是一种国际标准,旨在捕捉各种语言的字符(参见边栏 7.3)。Unicode 的前 128 个字符(\u0000\u007f)与 ASCII 相同。C 和 C++ 提供常规字符和“宽”字符,但对于宽字符,编码和实际宽度都取决于实现。Fortran 2003 支持四字节 Unicode 字符。

Characters have traditionally been implemented as one-byte quantities as well, typically (but not always) using the ASCII encoding. More recent languages (e.g., Java and C#) use a two-byte representation designed to accommodate (the commonly used portion of) the Unicode character set. Unicode is an international standard designed to capture the characters of a wide variety of languages (see Sidebar 7.3). The first 128 characters of Unicode (\u0000 through \u007f) are identical to ASCII. C and C++ provide both regular and “wide” characters, though for wide characters both the encoding and the actual width are implementation dependent. Fortran 2003 supports four-byte Unicode characters.

数字类型

Numeric Types

少数语言(例如 C 和 Fortran)区分不同长度的整数和实数;大多数语言不区分,而是将精度的选择留给实现。不幸的是,不同语言实现之间的精度差异导致缺乏可移植性:在一个系统上正确运行的程序可能会在另一个系统上产生运行时错误或错误结果。Java 和 C# 不同寻常的是,它们提供了几种长度的数字类型,并且每种类型都有指定的精度。

A few languages (e.g., C and Fortran) distinguish between different lengths of integers and real numbers; most do not, and leave the choice of precision to the implementation. Unfortunately, differences in precision across language implementations lead to a lack of portability: programs that run correctly on one system may produce run-time errors or erroneous results on another. Java and C# are unusual in providing several lengths of numeric types, with a specified precision for each.

一些语言,包括 C、C++、C# 和 Modula-2,提供有符号和无符号整数(Modula-2 将无符号整数称为基数)。一些语言(例如 Fortran、C、Common Lisp 和 Scheme)提供内置复数类型,通常实现为一对表示实部和虚部笛卡尔坐标的浮点数;其他语言将其作为标准库类支持。一些语言(例如 Scheme 和 Common Lisp)提供内置有理数类型,通常实现为一对表示分子和分母的整数。大多数 Lisp 变体也支持任意精度的整数,大多数脚本语言也是如此;实现在适当的情况下使用多个内存字。Ada 支持定点类型,它们在内部由整数表示,但在程序员指定的数字位置处有一个隐含的小数点。几种语言支持十进制 使用十进制编码的类型来避免财务和以人为本的算术中的舍入异常(参见边栏 7.4)。

A few languages, including C, C++, C#, and Modula-2, provide both signed and unsigned integers (Modula-2 calls unsigned integers cardinals). A few languages (e.g., Fortran, C, Common Lisp, and Scheme) provide a built-in complex type, usually implemented as a pair of floating-point numbers that represent the real and imaginary Cartesian coordinates; other languages support these as a standard library class. A few languages (e.g., Scheme and Common Lisp) provide a built-in rational type, usually implemented as a pair of integers that represent the numerator and denominator. Most varieties of Lisp also support integers of arbitrary precision, as do most scripting languages; the implementation uses multiple words of memory where appropriate. Ada supports fixed-point types, which are represented internally by integers, but have an implied decimal point at a programmer-specified position among the digits. Several languages support decimal types that use a base-10 encoding to avoid round-off anomalies in financial and human-centered arithmetic (see Sidebar 7.4).

整数、布尔值和字符都是离散类型(也称为序数类型)的例子:它们对应的域是可数的(它们与整数的某个子集一一对应),并且对于除第一个和最后一个之外的每个元素都有明确定义的前任和后继概念。(在大多数实现中,可能的整数数量是有限的,但这通常不会反映在类型系统中。)两种用户定义类型,枚举和子范围,也是离散的。离散、有理、实数和复杂类型共同构成标量类型。标量类型有时也称为简单类型。

Integers, Booleans, and characters are all examples of discrete types (also called ordinal types): the domains to which they correspond are countable (they have a one-to-one correspondence with some subset of the integers), and have a well-defined notion of predecessor and successor for each element other than the first and the last. (In most implementations the number of possible integers is finite, but this is usually not reflected in the type system.) Two varieties of user-defined types, enumerations and subranges, are also discrete. Discrete, rational, real, and complex types together constitute the scalar types. Scalar types are also sometimes called simple types.

设计与实现

Design & Implementation

7.3 多语言字符集

7.3 Multilingual character sets

ISO 10646国际标准定义了通用字符集 (UCS),旨在涵盖所有已知人类语言的所有字符。(它还为克林贡语、滕格瓦语和 Cirth [托尔金精灵语] 等人造 [构造] 语言留出“私有使用区域”。此私有空间的分配由名为 ConScript Unicode Registry 的志愿者组织协调。)目前,所有自然语言都使用 16 位基本多语言平面 (BMP)中的代码:0x00000xfffd

The ISO 10646 international standard defines a Universal Character Set (UCS) intended to include all characters of all known human languages. (It also sets aside a “private use area” for such artificial [constructed] languages as Klingon, Tengwar, and Cirth [Tolkien Elvish]. Allocation of this private space is coordinated by a volunteer organization known as the ConScript Unicode Registry.) All natural languages currently employ codes in the 16-bit Basic Multilingual Plane (BMP): 0x0000 through 0xfffd.

Unicode是 ISO 10646 的扩展版本,由国际软件制造商联盟维护。除了映射表之外,它还涵盖渲染算法、文本方向性以及排序和比较约定等主题。

Unicode is an expanded version of ISO 10646, maintained by an international consortium of software manufacturers. In addition to mapping tables, it covers such topics as rendering algorithms, directionality of text, and sorting and comparison conventions.

虽然最近的语言已转向使用 16 位或 32 位内部字符表示,但是这些字符不能用于外部存储(文本文件),否则会导致严重的向后兼容性问题。为了在不破坏现有工具的情况下适应 Unicode,Ken Thompson 于 1992 年提出了一种多字节“扩展”代码,即UTF-8(UCS/Unicode 转换格式,8 位),并编纂为 ISO 10646 的正式附件(附录)。UTF-8 字符最多占用 6 个字节——如果它们位于 BMP 中则为 3 个字节,如果是普通 ASCII 则仅占用 1 个字节。诀窍在于观察 ASCII 是 7 位代码;在任何旧式文本文件中,每个字节的最高有效位都是 0。在 UTF-8 中,最高有效位 1 表示多字节字符。双字节代码以位110开头。三字节代码以1110开头。多字节字符的第二个和后续字节始终以10开头。

While recent languages have moved toward 16- or 32-bit internal character representations, these cannot be used for external storage—text files—without causing severe problems with backward compatibility. To accommodate Unicode without breaking existing tools, Ken Thompson in 1992 proposed a multibyte “expanding” code known as UTF-8 (UCS/Unicode Transformation Format, 8-bit), and codified as a formal annex (appendix) to ISO 10646. UTF-8 characters occupy a maximum of 6 bytes—3 if they lie in the BMP, and only 1 if they are ordinary ASCII. The trick is to observe that ASCII is a 7-bit code; in any legacy text file the most significant bit of every byte is 0. In UTF-8 a most significant bit of 1 indicates a multibyte character. Two-byte codes begin with the bits 110. Three-byte codes begin with 1110. Second and subsequent bytes of multibyte characters always begin with 10.

在某些系统上,人们还会发现使用旧版 8 位 ISO 8859 标准的十种变体之一编码的文件,但这些文件在不同平台上的呈现方式不一致。在网络上,非 ASCII 字符通常使用数字字符引用进行编码,这些引用将十进制或十六进制的 Unicode 值括起来,并用“&”符号和分号括起来。例如,版权符号 (©) 为© ;。许多字符也有符号实体名称(例如©),但并非所有浏览器都支持这些名称。

On some systems one also finds files encoded in one of ten variants of the older 8-bit ISO 8859 standard, but these are inconsistently rendered across platforms. On the web, non-ASCII characters are typically encoded with numeric character references, which bracket a Unicode value, written in decimal or hex, with an ampersand and a semicolon. The copyright symbol (©), for example, is &#169;. Many characters also have symbolic entity names (e.g., &copy;), but not all browsers support these.

枚举类型

Enumeration Types

例 7.9

Example 7.9

Pascal 中的枚举

Enumerations in Pascal

枚举是 Wirth 在 Pascal 的设计中引入的。它们有助于创建可读的程序,并允许编译器捕获某些类型的编程错误。枚举类型由一组命名元素组成。在 Pascal 中,可以这样写

Enumerations were introduced by Wirth in the design of Pascal. They facilitate the creation of readable programs, and allow the compiler to catch certain kinds of programming errors. An enumeration type consists of a set of named elements. In Pascal, one could write

类型工作日 = (周日,周一,周二,周三,周四,周五,周六);

type weekday = (sun, mon, tue, wed, thu, fri, sat);

枚举类型的值是有序的,因此比较通常是有效的(mon < tue),并且通常有一种机制来确定枚举值的前任或后继(在 Pascal 中,tomorrow := succ(today))。枚举的有序性质有助于编写枚举控制循环:

The values of an enumeration type are ordered, so comparisons are generally valid (mon < tue), and there is usually a mechanism to determine the predecessor or successor of an enumeration value (in Pascal, tomorrow := succ(today)). The ordered nature of enumerations facilitates the writing of enumeration-controlled loops:

今天:= 周一到周五开始……

for today := mon to fri do begin …

设计与实现

Design & Implementation

7.4 十进制类型

7.4 Decimal types

一些语言(尤其是 Cobol 和 PL/I)提供了十进制类型,用于整数的定点表示。这些类型主要是为了利用许多传统 CISC 机器支持的二进制编码十进制 (BCD)整数格式而设计的。BCD 将一个半字节(四位 - 半个字节)专用于每个十进制数字。硬件支持 BCD 的机器可以直接对数字的 BCD 表示执行算术运算,而无需将其转换为二进制形式或从二进制形式转换为二进制形式。此功能在商业和金融应用程序中特别有用,这些应用程序将其数据视为数字和字符串。

A few languages, notably Cobol and PL/I, provide a decimal type for fixed-point representation of integer quantities. These types were designed primarily to exploit the binary-coded decimal (BCD) integer format supported by many traditional CISC machines. BCD devotes one nibble (four bits—half a byte) to each decimal digit. Machines that support BCD in hardware can perform arithmetic directly on the BCD representation of a number, without converting it to and from binary form. This capability is particularly useful in business and financial applications, which treat their data as both numbers and character strings.

随着网上商务的发展,过去几年人们对十进制算术的兴趣又重新燃起。2008 年修订的 IEEE 754 浮点标准包括 32 位、64 位和 128 位长度的十进制浮点类型。它们以二进制表示尾数(有效位)和指数,但将指数解释为十的幂,而不是二的幂。在给定长度下,十进制类型的值比二进制浮点值具有更高的精度,但范围更小。它们是财务计算的理想选择,因为它们可以精确捕获小数。设计人员希望新标准不仅在硬件中而且在软件库中取代现有的不兼容十进制格式,从而提供与原始 754 标准为二进制浮点提供的相同的可移植性和可预测性。

With the growth in on-line commerce, the past few years have seen renewed interest in decimal arithmetic. The 2008 revision of the IEEE 754 floating-point standard includes decimal floating-point types in 32-, 64-, and 128-bit lengths. These represent both the mantissa (significant bits) and exponent in binary, but interpret the exponent as a power of ten, not a power of two. At a given length, values of decimal type have greater precision but smaller range than binary floating-point values. They are ideal for financial calculations, because they capture decimal fractions precisely. Designers hope the new standard will displace existing incompatible decimal formats, not only in hardware but also in software libraries, thereby providing the same portability and predictability that the original 754 standard provided for binary floating-point.

C# 包含一个与新标准兼容的 128 位十进制类型。具体来说,C# 十进制变量包含 96 位精度、一个符号和一个十进制缩放因子,该因子可在 10 −28和 10 28之间变化。IBM 一直以商业和金融应用为重要市场,从 POWER6 开始,IBM 已在其 pSeries RISC 机器中包括该标准的硬件实现(64 位和 128 位宽度)。

C# includes a 128-bit decimal type that is compatible with the new standard. Specifically, a C# decimal variable includes 96 bits of precision, a sign, and a decimal scaling factor that can vary between 10−28 and 1028. IBM, for which business and financial applications have always been an important market, has included a hardware implementation of the standard (64- and 128-bit widths) in its pSeries RISC machines, beginning with the POWER6.

它还允许使用枚举来索引数组:

It also allows enumerations to be used to index arrays:

var daily_attendance:整数数组[weekday];

var daily_attendance : array [weekday] of integer;

例 7.10

Example 7.10

枚举作为常量

Enumerations as constants

当然,枚举的替代方法是简单地声明一个常量集合:

An alternative to enumerations, of course, is simply to declare a collection of constants:

const 太阳 = 0; 星期一 = 1; 星期二 = 2; 星期三 = 3; 星期四 = 4; 星期五 = 5; 星期六 = 6;

const sun = 0; mon = 1; tue = 2; wed = 3; thu = 4; fri = 5; sat = 6;

在 C 语言中,两种方法之间的区别纯粹是语法上的:

In C, the difference between the two approaches is purely syntactic:

枚举工作日 {周日,周一,周二,周三,周四,周五,周六};

enum weekday {sun, mon, tue, wed, thu, fri, sat};

本质上相当于

is essentially equivalent to

typedef int 星期几;

typedef int weekday;

const 工作日 太阳 = 0, 星期一 = 1, 星期二 = 2,

const weekday sun = 0, mon = 1, tue = 2,

 周三 = 3,周四 = 4,周五 = 5,周六 = 6;

 wed = 3, thu = 4, fri = 5, sat = 6;

然而,在 Pascal 及其大多数后代中,枚举和一组整数常量之间的差异更为显著:枚举是一种成熟的类型,与整数不兼容。在期望使用整数或枚举值的上下文中使用另一个值将导致编译时出现类型冲突错误。

In Pascal and most of its descendants, however, the difference between an enumeration and a set of integer constants is much more significant: the enumeration is a full-fledged type, incompatible with integers. Using an integer or an enumeration value in a context expecting the other will result in a type clash error at compile time.

例 7.11

Example 7.11

枚举类型与枚举类型的相互转换

Converting to and from enumeration type

枚举类型的值通常用小整数表示,通常是从零开始的连续小整数范围。在许多语言中,这些序数值具有语义意义,因为内置函数可用于将枚举值转换为其序数值,有时反之亦然。在 Ada 中,这些转换使用属性pos val :weekday' pos(mon) = 1weekday'val(1) = mon。■

Values of an enumeration type are typically represented by small integers, usually a consecutive range of small integers starting at zero. In many languages these ordinal values are semantically significant, because built-in functions can be used to convert an enumeration value to its ordinal value, and sometimes vice versa. In Ada, these conversions employ the attributes pos and val: weekday' pos(mon) = 1 and weekday'val(1) = mon. ■

例 7.12

Example 7.12

枚举的杰出值

Distinguished values for enums

如果不希望使用默认赋值,有几种语言允许程序员指定枚举类型的序数值。在 C、C++ 和 C# 中,可以这样写

Several languages allow the programmer to specify the ordinal values of enumeration types, if the default assignment is undesirable. In C, C++, and C#, one could write

枚举 arm_special_regs {fp =7,sp = 13,lr = 14,pc = 15};

enum arm_special_regs {fp =7, sp = 13, lr = 14, pc = 15};

(这些值背后的直觉在第 C-5.4.5 和C-9.2.2节中进行了解释。)

(The intuition behind these values is explained in Sections C-5.4.5 and C-9.2.2.)

在 Ada 中,这个声明应该写成

In Ada this declaration would be written

type arm_special_regs 是 (fp, sp, lr, pc); -- 必须排序

type arm_special_regs is (fp, sp, lr, pc); -- must be sorted

对于 arm_special_regs 使用 (fp => 7, sp => 13, lr => 14, pc => 15);

for arm_special_regs use (fp => 7, sp => 13, lr => 14, pc => 15);

例 7.13

Example 7.13

在 Java 中模拟不同的枚举值

Emulating distinguished enum values in Java

在 Java 的最新版本中,可以通过为值添加一个额外的字段(此处名为register )来获得类似的效果:

In recent versions of Java one can obtain a similar effect by giving values an extra field (here named register):

枚举 arm_special_regs { fp(7), sp(13), lr(14), pc(15);

enum arm_special_regs { fp(7), sp(13), lr(14), pc(15);

 私人最终整数寄存器;

 private final int register;

 arm_special_regs(int r) { 寄存器 = r; }

 arm_special_regs(int r) { register = r; }

 public int reg() { 返回寄存器; }

 public int reg() { return register; }

}

}

int n = arm_special_regs.fp.reg();

int n = arm_special_regs.fp.reg();

如第 3.5.2 节所述,Pascal 和 C 不允许在同一范围内的多个枚举类型中使用相同的元素名称。Java 和 C# 允许,但程序员必须使用完全限定名称来标识元素:arm_special_regs.fp。Ada放宽了这一要求,规定元素名称是重载的;只要编译器可以根据上下文推断出类型前缀,就可以省略它(示例 3.22)。C++ 在禁止重复枚举名称方面历来模仿 C。C++11 引入了一种新的枚举类型,它模仿 Java 和 C#(示例 3.23)。

As noted in Section 3.5.2, Pascal and C do not allow the same element name to be used in more than one enumeration type in the same scope. Java and C# do, but the programmer must identify elements using fully qualified names: arm_special_regs.fp. Ada relaxes this requirement by saying that element names are overloaded; the type prefix can be omitted whenever the compiler can infer it from context (Example 3.22). C++ historically mirrored C in prohibiting duplicate enum names. C++11 introduced a new variety of enum that mirrors Java and C# (Example 3.23).

子范围类型

Subrange Types

例 7.14

Example 7.14

Pascal 中的子范围

Subranges in Pascal

与枚举一样,子范围最初是在 Pascal 中引入的,并出现在许多后续语言中。子范围是一种类型,其值由某个离散类型(也称为类型)的值的连续子集组成。在 Pascal 及其大多数后代中,可以声明整数、字符、枚举甚至其他子范围的子范围。在 Pascal 中,子范围如下所示:

Like enumerations, subranges were first introduced in Pascal, and are found in many subsequent languages. A subrange is a type whose values compose a contiguous subset of the values of some discrete base type (also called the parent type). In Pascal and most of its descendants, one can declare subranges of integers, characters, enumerations, and even other subranges. In Pascal, subranges looked like this:

类型 test_score = 0..100;

type test_score = 0..100;

 工作日 = 星期一..星期五;

 workday = mon..fri;

设计与实现

Design & Implementation

7.5 整数的多种大小

7.5 Multiple sizes of integers

Pascal 和 Ada 中 (小值) 子范围类型可以节省空间,而其他几种语言则通过提供多种大小的内置整数类型来实现这一点。例如,C 和 C++ 支持对charshortintlonglong long类型的有符号和无符号变体进行整数运算,大小单调不减少。2

The space savings possible with (small-valued) subrange types in Pascal and Ada is achieved in several other languages by providing more than one size of built-in integer type. C and C++, for example, support integer arithmetic on signed and unsigned variants of char, short, int, long, and long long types, with monotonically nondecreasing sizes.2

例 7.15

Example 7.15

Ada 中的子范围

Subranges in Ada

在 Ada 中可以写成

In Ada one would write

类型 test_score 是新的整数范围 0..100;

type test_score is new integer range 0..100;

子类型工作日是工作日范围周一至周五;

subtype workday is weekday range mon..fri;

Ada 中定义的 range… 部分称为类型约束此示例中,test_score派生类型,与整数不兼容。另一方面,workday类型是受约束的子类型工作日工作日可以或多或少自由混合。派生类型和子类型之间的区别是 Ada 的一个宝贵特性;我们将在第 7.2.1 节中进一步讨论它。■

The range… portion of the definition in Ada is called a type constraint. In this example test_score is a derived type, incompatible with integers. The workday type, on the other hand, is a constrained subtype; workdays and weekdays can be more or less freely intermixed. The distinction between derived types and subtypes is a valuable feature of Ada; we will discuss it further in Section 7.2.1. ■

当然,也可以用整数来表示测试分数,或者用工作日来表示工作日。使用显式子范围有几个优点。首先,它有助于记录程序。注释也可以用作文档,但是注释有一个坏习惯,就是随着程序的变化而变得过时,或者一开始就被省略。因为编译器会分析子范围声明,所以它知道子范围值的预期范围,并可以生成代码来执行动态语义检查,以确保没有子范围变量被赋予无效值。这些检查可以成为有价值的调试工具。此外,由于编译器知道子范围中值的数量,所以它有时可以使用比表示任意整数所需更少的位来表示子范围值。在上面的例子中,test_score值可以存储在一个字节中。

One could of course use integers to represent test scores, or a weekday to represent a workday. Using an explicit subrange has several advantages. For one thing, it helps to document the program. A comment could also serve as documentation, but comments have a bad habit of growing out of date as programs change, or of being omitted in the first place. Because the compiler analyzes a subrange declaration, it knows the expected range of subrange values, and can generate code to perform dynamic semantic checks to ensure that no subrange variable is ever assigned an invalid value. These checks can be valuable debugging tools. In addition, since the compiler knows the number of values in the subrange, it can sometimes use fewer bits to represent subrange values than it would need to use to represent arbitrary integers. In the example above, test_score values can be stored in a single byte.

例 7.16

Example 7.16

子范围类型的空间要求

Space requirements of subrange type

大多数实现对整数和子范围采用相同的位模式,因此即使不同值的数量很少,值较大的子范围也需要较大的存储位置。例如,以下类型

Most implementations employ the same bit patterns for integers and subranges, so subranges whose values are large require large storage locations, even if the number of distinct values is small. The following type, for example,

类型水温 = 273..373; (* 开尔文度*)

type water_temperature = 273..373; (* degrees Kelvin *)

至少需要两个字节才能存储。虽然该类型只有 101 个不同的值,但最大的值 (373) 太大,无法用自然编码的单个字节容纳。(无符号字节可以保存 0.. 255 范围内的值;有符号字节可以保存 −128.. 127 范围内的值。)■

would be stored in at least two bytes. While there are only 101 distinct values in the type, the largest (373) is too large to fit in a single byte in its natural encoding. (An unsigned byte can hold values in the range 0.. 255; a signed byte can hold values in the range −128.. 127.) ■

复合类型

Composite Types

非标量类型通常称为复合类型。它们通常是通过将类型构造函数应用于一个或多个更简单的类型来创建的。我们在示例 7.6中介绍的Options可以说是最简单的复合类型,其作用仅仅是向某个任意基类型的值添加额外的“以上都不是”。其他常见的复合类型包括记录(结构)、变体记录(联合)、数组、集合、指针、列表和文件。除了指针和列表之外,其他所有类型都可以轻松地用数学集合运算来描述(指针和列表也可以用数学方式描述,但描述不太直观)。

Nonscalar types are usually called composite types. They are generally created by applying a type constructor to one or more simpler types. Options, which we introduced in Example 7.6, are arguably the simplest composite types, serving only to add an extra “none of the above” to the values of some arbitrary base type. Other common composite types include records (structures), variant records (unions), arrays, sets, pointers, lists, and files. All but pointers and lists are easily described in terms of mathematical set operations (pointers and lists can be described mathematically as well, but the description is less intuitive).

记录(结构)由 Cobol 引入,自 20 世纪 60 年代以来,大多数语言都支持它。记录由字段集合组成,每个字段属于(可能不同的)更简单的类型。记录类似于数学元组;记录类型对应于字段类型的笛卡尔积。

Records (structs) were introduced by Cobol, and have been supported by most languages since the 1960s. A record consists of collection of fields, each of which belongs to a (potentially different) simpler type. Records are akin to mathematical tuples; a record type corresponds to the Cartesian product of the types of the fields.

变体记录(联合)与“普通”记录的不同之处在于,变体记录的字段(或字段集合)中只有一个在给定时间内有效。变体记录类型是其字段类型的不相交联合,而不是其笛卡尔积。

Variant records (unions) differ from “normal” records in that only one of a variant record's fields (or collections of fields) is valid at any given time. A variant record type is the disjoint union of its field types, rather than their Cartesian product.

数组是最常用的复合类型。数组可以被认为是将索引类型的成员映射到组件类型的成员的函数。字符数组通常称为字符串,并且通常由其他数组所不具备的特殊用途操作支持。

Arrays are the most commonly used composite types. An array can be thought of as a function that maps members of an index type to members of a component type. Arrays of characters are often referred to as strings, and are often supported by special-purpose operations not available for other arrays.

集合,与枚举和子范围一样,是由 Pascal 引入的。集合类型是其基类型的数学幂集,通常必须是离散的。集合类型的变量包含基类型的不同元素的集合。

Sets, like enumerations and subranges, were introduced by Pascal. A set type is the mathematical powerset of its base type, which must often be discrete. A variable of a set type contains a collection of distinct elements of the base type.

指针是左值。指针值是对指针基类型的对象的引用。指针通常(但并非总是)实现为地址。它们最常用于实现递归数据类型。如果类型T的对象可能包含一个或多个对类型T的其他对象的引用,则类型T是递归的。

Pointers are l-values. A pointer value is a reference to an object of the pointer's base type. Pointers are often but not always implemented as addresses. They are most often used to implement recursive data types. A type T is recursive if an object of type T may contain one or more references to other objects of type T.

列表与数组一样,包含元素序列,但没有映射或索引的概念。相反,列表以递归方式定义为空列表或由头元素和对子列表的引用组成的对。虽然大多数(但不是全部)语言都必须在阐述时指定数组的长度,但列表的长度始终是可变的。要找到列表中的给定元素,程序必须从头部开始递归或迭代地检查所有先前的元素。由于列表具有递归定义,因此列表是大多数函数式语言编程的基础。

Lists, like arrays, contain a sequence of elements, but there is no notion of mapping or indexing. Rather, a list is defined recursively as either an empty list or a pair consisting of a head element and a reference to a sublist. While the length of an array must be specified at elaboration time in most (though not all) languages, lists are always of variable length. To find a given element of a list, a program must examine all previous elements, recursively or iteratively, starting at the head. Because of their recursive definition, lists are fundamental to programming in most functional languages.

文件用于表示大容量存储设备上的数据,位于其他程序对象所在的内存之外。与数组一样,大多数文件可以概念化为将索引类型(通常是整数)的成员映射到组件类型的成员的函数。与数组不同,文件通常具有当前位置的概念,这允许在连续操作中隐式地暗示索引。文件通常会显示从物理输入/输出设备继承的特性。特别是,某些文件的元素必须按顺序访问。

Files are intended to represent data on mass-storage devices, outside the memory in which other program objects reside. Like arrays, most files can be conceptualized as a function that maps members of an index type (generally integer) to members of a component type. Unlike arrays, files usually have a notion of current position, which allows the index to be implied implicitly in consecutive operations. Files often display idiosyncrasies inherited from physical input/output devices. In particular, the elements of some files must be accessed in sequential order.

我们将在第 8 章中更详细地讨论复合类型。

We will examine composite types in more detail in Chapter 8.

在07-01-9780124104099检查你的理解

Check Your Understanding

1. 类型在编程语言中起什么作用?

1. What purpose(s) do types serve in a programming language?

2. 强类型语言是什么意思?静态类型?什么因素阻碍了 C 语言成为强类型?

2. What does it mean for a language to be strongly typed? Statically typed? What prevents, say, C from being strongly typed?

3. 说出两种强类型但动态类型的编程语言。

3. Name two programming languages that are strongly but dynamically typed.

4. 什么是类型冲突

4. What is a type clash?

5.讨论 类型的指称视图结构视图和基于抽象视图之间的差异。

5. Discuss the differences among the denotational, structural, and abstraction-based views of types.

6.一组语言特征(例如类型系统)的 正交意味着什么?

6. What does it mean for a set of language features (e.g., a type system) to be orthogonal?

7. 什么是聚合体?

7. What are aggregates?

8. 期权类型有哪些?它们有什么用途?

8. What are option types? What purpose do they serve?

9. 什么是多态性?它的参数化子类型变体有何区别?什么是泛型

9. What is polymorphism? What distinguishes its parametric and subtype varieties? What are generics?

10. 离散类型标量类型有什么区别?

10. What is the difference between discrete and scalar types?

11. 举两个缺乏布尔类型的语言的例子。它们用什么来代替?

11. Give two examples of languages that lack a Boolean type. What do they use instead?

12. 枚举类型在哪些方面优于命名常量的集合?子范围类型在哪些方面优于其基类型?字符串在哪些方面优于字符数组?

12. In what ways may an enumeration type be preferable to a collection of named constants? In what ways may a subrange type be preferable to its base type? In what ways may a string be preferable to an array of characters?

7.2 类型检查

7.2 Type Checking

在大多数静态类型语言中,对象(常量、变量、子程序等)的每个定义都必须指定对象的类型。此外,对象可能出现的许多上下文也是有类型的,从某种意义上说,语言规则限制了该上下文中的对象可以有效拥有的类型。在下面的小节中,我们将考虑类型等价类型兼容性类型推断的主题。在这三者中,类型兼容性是程序员最关心的。它决定了何时可以在某个上下文中使用某种类型的对象。至少,如果对象的类型和上下文期望的类型是等价的(即相同),则可以使用该对象。然而,在许多语言中,兼容性是一种比等价更松散的关系:对象和上下文往往兼容,即使它们的类型不同。我们对类型兼容性的讨论将涉及类型转换(也称为强制类型转换),它将一种类型的值转换为另一种类型的值;类型强制,在特定上下文中自动执行转换;非转换类型转换,有时在系统编程中使用,将一种类型的值的位解释为好像它们表示其他类型的值。

In most statically typed languages, every definition of an object (constant, variable, subroutine, etc.) must specify the object's type. Moreover, many of the contexts in which an object might appear are also typed, in the sense that the rules of the language constrain the types that an object in that context may validly possess. In the subsections below we will consider the topics of type equivalence, type compatibility, and type inference. Of the three, type compatibility is the one of most concern to programmers. It determines when an object of a certain type can be used in a certain context. At a minimum, the object can be used if its type and the type expected by the context are equivalent (i.e., the same). In many languages, however, compatibility is a looser relationship than equivalence: objects and contexts are often compatible even when their types are different. Our discussion of type compatibility will touch on the subjects of type conversion (also called casting), which changes a value of one type into a value of another; type coercion, which performs a conversion automatically in certain contexts; and nonconverting type casts, which are sometimes used in systems programming to interpret the bits of a value of one type as if they represented a value of some other type.

每当一个表达式由更简单的子表达式构造时,就会出现一个问题:给定子表达式的类型(以及可能期望的类型),如果表达式的类型是随机的(即上下文),那么整个表达式的类型是什么?类型推断可以回答这个问题。类型推断通常很简单:例如,两个整数之和仍然是整数。在其他情况下(例如,处理集合时),它会更加棘手。类型推断在 ML、Miranda 和 Haskell 中起着特别重要的作用,在这些语言中,几乎所有类型注释都是可选的,如果省略,编译器将推断类型。

Whenever an expression is constructed from simpler subexpressions, the question arises: given the types of the subexpressions (and possibly the type expected by the surrounding context), what is the type of the expression as a whole? This question is answered by type inference. Type inference is often trivial: the sum of two integers is still an integer, for example. In other cases (e.g., when dealing with sets) it is a good bit trickier. Type inference plays a particularly important role in ML, Miranda, and Haskell, in which almost all type annotations are optional, and will be inferred by the compiler when omitted.

7.2.1 类型等价

7.2.1 Type Equivalence

在用户可以定义新类型的语言中,定义类型等价有两种主要方式。结构等价基于类型定义的内容:粗略地说,如果两种类型由相同的组件组成,并且以相同的方式组合在一起,则它们是相同的。名称等价基于类型定义的词汇出现:粗略地说,每个定义都会引入一种新类型。结构等价用于 Algol-68、Modula-3 以及(有各种不同之处的)C 和 ML。名称等价出现在 Java、C#、标准 Pascal 和大多数 Pascal 后代(包括 Ada)中。

In a language in which the user can define new types, there are two principal ways of defining type equivalence. Structural equivalence is based on the content of type definitions: roughly speaking, two types are the same if they consist of the same components, put together in the same way. Name equivalence is based on the lexical occurrence of type definitions: roughly speaking, each definition introduces a new type. Structural equivalence is used in Algol-68, Modula-3, and (with various wrinkles) C and ML. Name equivalence appears in Java, C#, standard Pascal, and most Pascal descendants, including Ada.

例 7.17

Example 7.17

类型的细微差异

Trivial differences in type

结构等价性的确切定义因语言而异。它要求人们决定类型之间的哪些潜在差异是重要的,哪些可能不重要。大多数人可能同意声明的格式并不重要——仅在间距或换行符方面不同的相同声明仍应被视为等价。同样,在具有结构等价性的 Pascal 类语言中,

The exact definition of structural equivalence varies from one language to another. It requires that one decide which potential differences between types are important, and which maybe considered unimportant. Most people would probably agree that the format of a declaration should not matter—identical declarations that differ only in spacing or line breaks should still be considered equivalent. Likewise, in a Pascal-like language with structural equivalence,

类型 R1 = 记录

type R1 = record

 a,b:整数

 a, b : integer

结尾;

end;

应该被认为等同于

should probably be considered the same as

类型 R2 = 记录

type R2 = record

 a:整数;

 a : integer;

 b :整数

 b : integer

结尾;

end;

但是

But what about

类型 R3 = 记录

type R3 = record

 b:整数;

 b : integer;

 a :整数

 a : integer

结尾;

end;

字段顺序的反转是否应该改变类型?ML 说不;大多数语言说是。■

Should the reversal of the order of the fields change the type? ML says no; most languages say yes. ■

例 7.18

Example 7.18

类型的其他细微差异

Other minor differences in type

类似地,考虑以下数组,同样采用类似 Pascal 的表示法:

In a similar vein, consider the following arrays, again in a Pascal-like notation:

类型 str = char 数组[1..10];

type str = array [1..10] of char;

类型 str = char 数组[0..9];

type str = array [0..9] of char;

此处数组的长度在两种情况下相同,但索引值不同。这些应该被视为等价吗?大多数语言都认为不是,但有些语言(包括 Fortran 和 Ada)认为它们是兼容的。■

Here the length of the array is the same in both cases, but the index values are different. Should these be considered equivalent? Most languages say no, but some (including Fortran and Ada) consider them compatible. ■

要确定两种类型在结构上是否等效,编译器可以通过用其各自的定义替换任何嵌入的类型名称来扩展它们的定义,递归地进行,直到只剩下一长串类型构造函数、字段名称和内置类型。如果这些扩展的字符串相同,则类型等效,反之亦然。递归和基于指针的类型使问题变得复杂,因为它们的扩展不会终止,但问题并非无法克服;我们在练习 8.15中考虑了一个解决方案。

To determine if two types are structurally equivalent, a compiler can expand their definitions by replacing any embedded type names with their respective definitions, recursively, until nothing is left but a long string of type constructors, field names, and built-in types. If these expanded strings are the same, then the types are equivalent, and conversely. Recursive and pointer-based types complicate matters, since their expansion does not terminate, but the problem is not insurmountable; we consider a solution in Exercise 8.15.

例 7.19

Example 7.19

结构等价问题

The problem with structural equivalence

结构等价性是一种简单但有点低级、面向实现的思考类型的方法。它的主要问题是无法区分程序员可能认为不同但碰巧具有相同内部结构的类型:

Structural equivalence is a straightforward but somewhat low-level, implementation-oriented way to think about types. Its principal problem is an inability to distinguish between types that the programmer may think of as distinct, but which happen by coincidence to have the same internal structure:

1. 输入 student = record

1. type student = record

2. 姓名,地址:字符串

2.    name, address : string

3.年龄:整数

3.    age : integer

4. 输入学校 = 记录

4. type school = record

5. 姓名,地址:字符串

5.    name, address : string

6.年龄:整数

6.    age : integer

7. x:学生;

7. x : student;

8. y:学校;

8. y : school;

9. …

9. …

10. x :=y;——这是一个错误吗?

10. x :=y; --is this an error?

大多数程序员可能都希望在无意中将school类型的值赋给student类型的变量时得到通知,但基于结构等价性类型检查的编译器将会欣然接受这样的赋值。

Most programmers would probably want to be informed if they accidentally assigned a value of type school into a variable of type student, but a compiler whose type checking is based on structural equivalence will blithely accept such an assignment.

名称等价性基于这样的假设:如果程序员费力编写两个类型定义,那么这些定义可能表示不同的类型。在上面的例子中,变量xy在名称等价性下将被视为具有不同的类型:x使用第 1 行声明的类型;y使用第 4 行声明的类型。■

Name equivalence is based on the assumption that if the programmer goes to the effort of writing two type definitions, then those definitions are probably meant to represent different types. In the example above, variables x and y will be considered to have different types under name equivalence: x uses the type declared at line 1; y uses the type declared at line 4. ■

名称等价的变体

Variants of Name Equivalence

例 7.20

Example 7.20

别名类型

Alias types

名称等价性的使用中的一个微妙之处出现在最简单的类型声明中:

One subtlety in the use of name equivalence arises in the simplest of type declarations:

类型新类型=旧类型;(* Algol 系列语法 *)
typedef 旧类型 新类型;/* C 系列语法 */

这里称new_typeold_type的别名我们应该把它们当作同一类型的两个名字,还是当作恰巧有相同内部结构的两种不同类型的名字?“正确”的方法可能因程序而异。■

Here new_type is said to be an alias for old_type. Should we treat them as two names for the same type, or as names for two different types that happen to have the same internal structure? The “right” approach may vary from one program to another. ■

例 7.21

Example 7.21

语义等效的别名类型

Semantically equivalent alias types

任何类 Unix 系统的用户都熟悉文件权限位的概念。这些权限位指定文件是否可由其所有者、组成员或其他人读取、写入和/或执行。在系统库中,文件的权限集表示为mode_t类型的值。在 C 中,此类型通常定义为预定义的 16 位无符号整数类型的别名:

Users of any Unix-like system will be familiar with the notion of permission bits on files. These specify whether the file is readable, writable, and/or executable by its owner, group members, or others. Within the system libraries, the set of permissions for a file is represented as a value of type mode_t. In C, this type is commonly defined as an alias for the predefined 16-bit unsigned integer type:

类型定义 uint16_t mode_t;

typedef uint16_t mode_t;

虽然 C 对标量类型使用结构等价性,我们可以想象如果它统一使用名称等价性会出现什么问题。按照惯例,权限集使用按位整数运算符进行操作:

While C uses structural equivalence for scalar types,3 we can imagine the issue that would arise if it used name equivalence uniformly. By convention, permission sets are manipulated using bitwise integer operators:

mode_t my_permissions = S_IRUSR | S_IWUSR | S_IRGRP;

mode_t my_permissions = S_IRUSR | S_IWUSR | S_IRGRP;

/* 我可以读写;我的小组成员可以阅读。 */

/* I can read and write; members of my group can read. */

如果(my_permissions&S_IWUSR)…

if (my_permissions & S_IWUSR) …

此约定取决于mode_tuint16_t的等价性。可以要求程序员在应用整数运算符之前将mode_t对象显式转换为uint_16 — 甚至可以建议将mode_t设为抽象类型,使用插入移除查找操作隐藏内部表示 — 但 C 程序员可能会认为这两种选择都过于繁琐:在“系统”代码中,将mode_tuint16_t视为相同似乎是合理的。■

This convention depends on the equivalence of mode_t and uint16_t. One could ask programmers to convert mode_t objects explicitly to uint_16 before applying an integer operator—or even suggest that mode_t be an abstract type, with insert, remove, and lookup operations that hide the internal representation—but C programmers would probably regard either of these options as unnecessarily cumbersome: in “systems” code, it seems reasonable to treat mode_t and uint16_t the same. ■

例 7.22

Example 7.22

语义上不同的别名类型

Semantically distinct alias types

不幸的是,有时别名类型可能不应该相同:

Unfortunately, there are other times when aliased types should probably not be the same:

类型 celsius_temp = real;

type celsius_temp = real;

 华氏温度 = 实际温度;

 fahrenheit_temp = real;

var c: 摄氏度;

var c : celsius_temp;

 f:华氏温度;

 f : fahrenheit_temp;

f := c; (* 这可能是一个错误 *)

f := c; (* this should probably be an error *)

例 7.23

Example 7.23

Ada 中的派生类型和子类型

Derived types and subtypes in Ada

如果一种语言中别名类型被视为不同,则称其具有严格名称等价性。如果一种语言中别名类型被视为等价,则称其具有松散名称等价性。大多数 Pascal 家族语言都使用松散名称等价性。Ada 通过允许程序员指示别名是代表派生类型还是子类型,实现了两全其美。子类型是与其基类型兼容;派生类型不兼容。(相同基类型的子类型也相互兼容。)我们上面的例子可以写成

A language in which aliased types are considered distinct is said to have strict name equivalence. A language in which aliased types are considered equivalent is said to have loose name equivalence. Most Pascal-family languages use loose name equivalence. Ada achieves the best of both worlds by allowing the programmer to indicate whether an alias represents a derived type or a subtype. A subtype is compatible with its base (parent) type; a derived type is incompatible. (Subtypes of the same base type are also compatible with each other.) Our examples above would be written

子类型 mode_t 是整数范围 0..2**16-1;--无符号 16 位整数

subtype mode_t is integer range 0..2**16-1; -- unsigned 16-bit integer

类型 celsius_temp 是新的整数;

type celsius_temp is new integer;

类型 fahrenheit_temp 是新的整数;

type fahrenheit_temp is new integer;

理解严格名称等价和宽松名称等价之间的区别的一种方法是记住声明和定义之间的区别(第 3.3.3 节)。在严格名称等价下,声明类型 A = B被视为定义。在宽松名称等价下,它仅仅是一个声明;A共享B的定义。

One way to think about the difference between strict and loose name equivalence is to remember the distinction between declarations and definitions (Section 3.3.3). Under strict name equivalence, a declaration type A = B is considered a definition. Under loose name equivalence it is merely a declaration; A shares the definition of B.

例 7.24

Example 7.24

名称与结构等价性

Name vs structural equivalence

请考虑以下示例:

Consider the following example:

1. type cell =… --whatever

1. type cell =… --whatever

2. 类型 alink = 指向单元格的指针

2. type alink = pointer to cell

3. 输入 blink = alink

3. type blink = alink

4. p, q :指向单元格的指针

4. p, q : pointer to cell

5. r:alink

5. r : alink

6. s :闪烁

6. s : blink

7. t:指向单元格的指针

7. t : pointer to cell

8. u:alink

8. u : alink

这里第 3 行的声明是一个别名;它将 blink 定义为“与” alink “相同” 。在严格名称等价下,第 3 行既是声明又是定义,并且blink是一种新类型,不同于alink。在宽松名称等价下,第 3 行只是一个声明;它使用第 2 行的定义。

Here the declaration at line 3 is an alias; it defines blink to be “the same as” alink. Under strict name equivalence, line 3 is both a declaration and a definition, and blink is a new type, distinct from alink. Under loose name equivalence, line 3 is just a declaration; it uses the definition at line 2.

在严格名称等价下,p 和 q 具有相同的类型,因为它们都使用第 4 行右侧的匿名(未命名)类型定义,而ru具有相同的类型,因为它们都使用第 2 行的定义。在宽松名称等价下,rsu都具有相同的类型, pq也是如此。在结构等价下,所示的所有六个变量都具有相同的类型,即指向任何单元格的指针。■

Under strict name equivalence, p and q have the same type, because they both use the anonymous (unnamed) type definition on the right-hand side of line 4, and r and u have the same type, because they both use the definition at line 2. Under loose name equivalence, r, s, and u all have the same type, as do p and q. Under structural equivalence, all six of the variables shown have the same type, namely pointer to whatever cell is. ■

在单独编译的情况下,结构等价和名称等价都很难实现。我们将在15.6 节中讨论这个问题。

Both structural and name equivalence can be tricky to implement in the presence of separate compilation. We will return to this issue in Section 15.6.

类型转换和强制类型转换

Type Conversion and Casts

例 7.25

Example 7.25

需要给定类型的上下文

Contexts that expect a given type

在静态类型的语言中,许多情况下都需要特定类型的值。在语句中

In a language with static typing, there are many contexts in which values of a specific type are expected. In the statement

a := 表达式

a := expression

我们期望右侧具有与 相同的类型在表达式中

we expect the right-hand side to have the same type as a. In the expression

a + b

a + b

重载的 + 符号表示整数或浮点加法;因此我们期望ab都是整数,或者都是实数。在调用子程序时,

the overloaded + symbol designates either integer or floating-point addition; we therefore expect either that a and b will both be integers, or that they will both be reals. In a call to a subroutine,

foo(arg1,arg2,...,argN)

foo(arg1, arg2, …, argN)

我们期望参数的类型与形式参数的类型相匹配,就像在子程序的头部声明的那样。■

we expect the types of the arguments to match those of the formal parameters, as declared in the subroutine's header. ■

假设我们目前要求在每种情况下类型(预期和提供)完全相同。那么,如果程序员希望在需要另一种类型的上下文中使用一种类型的值,他或她将需要指定显式类型转换(有时也称为类型转换)。根据所涉及的类型,转换可能需要或不需要在运行时执行代码。主要有三种情况:

Suppose for the moment that we require in each of these cases that the types (expected and provided) be exactly the same. Then if the programmer wishes to use a value of one type in a context that expects another, he or she will need to specify an explicit type conversion (also sometimes called a type cast). Depending on the types involved, the conversion may or may not require code to be executed at run time. There are three principal cases:

1. 类型在结构上被视为等价,但语言使用名称等价。在这种情况下,类型采用相同的低级表示,并具有相同的值集。因此,转换是纯粹的概念操作;运行时不需要执行任何代码。

1. The types would be considered structurally equivalent, but the language uses name equivalence. In this case the types employ the same low-level representation, and have the same set of values. The conversion is therefore a purely conceptual operation; no code will need to be executed at run time.

2. 类型具有不同的值集,但相交值的表示方式相同。例如,一种类型可能是另一种类型的子范围,或者一种类型可能由二进制补码有符号整数组成,而另一种类型是无符号的。如果提供的类型具有预期类型所没有的一些值,则必须在运行时执行代码以确保当前值在预期类型中有效。如果检查失败,则会导致动态语义错误。如果检查成功,则可以使用该值的底层表示,无需更改。某些语言实现可能允许禁用检查,从而导致速度更快但可能不安全的代码。

2. The types have different sets of values, but the intersecting values are represented in the same way. One type may be a subrange of the other, for example, or one may consist of two's complement signed integers, while the other is unsigned. If the provided type has some values that the expected type does not, then code must be executed at run time to ensure that the current value is among those that are valid in the expected type. If the check fails, then a dynamic semantic error results. If the check succeeds, then the underlying representation of the value can be used, unchanged. Some language implementations may allow the check to be disabled, resulting in faster but potentially unsafe code.

3. 类型具有不同的低级表示,但我们仍然可以定义它们的值之间的某种对应关系。例如,32 位整数可以转换为双精度 IEEE 浮点数,而不会损失精度。大多数处理器都提供了机器指令来实现此转换。浮点数可以通过四舍五入或截断转换为整数,但小数位会丢失,并且转换将溢出许多指数值。同样,大多数处理器都提供了机器指令来实现此转换。不同长度的整数之间的转换可以通过丢弃或符号扩展高位字节来实现。

3. The types have different low-level representations, but we can nonetheless define some sort of correspondence among their values. A 32-bit integer, for example, can be converted to a double-precision IEEE floating-point number with no loss of precision. Most processors provide a machine instruction to effect this conversion. A floating-point number can be converted to an integer by rounding or truncating, but fractional digits will be lost, and the conversion will overflow for many exponent values. Again, most processors provide a machine instruction to effect this conversion. Conversions between different lengths of integers can be effected by discarding or sign-extending high-order bytes.

例 7.26

Example 7.26

Ada 中的类型转换

Type conversions in Ada

我们可以用以下 Ada 中类型转换的示例来说明这些选项:

We can illustrate these options with the following examples of type conversions in Ada:

n:整数;-- 假设为 32 位
r:长整型浮点数;-- 假设 IEEE 双精度
t:测试分数; ——如示例 7.15所示
c:摄氏温度; ——如示例 7.23所示
t:=测试分数(n);-- 需要运行时语义检查
n:=整数(t);-- 无需检查;每个 test_score 都是一个 int
r:= long_float(n);-- 需要运行时转换
n := 整数(r);-- 需要运行时转换和检查
n:=整数(c);-- 无需运行时代码
c:=摄氏度_温度(n);-- 无需运行时代码

在六个任务中的每一个中,类型的名称都用作执行类型转换的伪函数。第一个转换需要运行时检查以确保n的值在test_score的范围内。第二个转换不需要代码,因为t的每个可能值对于n都是可接受的。第三个和第四个转换需要代码来更改值的低级表示。第四个转换还需要语义检查。通常认为,从浮点值转换为整数会导致小数位丢失;这种丢失不是错误。但是,如果转换导致整数溢出,则需要导致错误。最后两个转换不需要运行时代码;整数和celsius_temp 类型(至少正如我们所定义的)具有相同的值集和相同的底层表示。纯粹主义者可能会说celsius_temp应该定义为新的整数范围 -273..integer' last,在这种情况下,需要对最后的转换进行运行时语义检查。 ■

In each of the six assignments, the name of a type is used as a pseudofunction that performs a type conversion. The first conversion requires a run-time check to ensure that the value of n is within the bounds of a test_score. The second conversion requires no code, since every possible value of t is acceptable for n. The third and fourth conversions require code to change the low-level representation of values. The fourth conversion also requires a semantic check. It is generally understood that converting from a floating-point value to an integer results in the loss of fractional digits; this loss is not an error. If the conversion results in integer overflow, however, an error needs to result. The final two conversions require no run-time code; the integer and celsius_temp types (at least as we have defined them) have the same sets of values and the same underlying representation. A purist might say that celsius_temp should be defined as new integer range -273..integer' last, in which case a run-time semantic check would be required on the final conversion. ■

例 7.27

Example 7.27

C 中的类型转换

Type conversions in C

C 中的类型转换(C 称之为类型强制转换)是通过使用所需类型的名称(在括号中)作为前缀运算符来指定的:

A type conversion in C (what C calls a type cast) is specified by using the name of the desired type, in parentheses, as a prefix operator:

r = (浮点数)n;/* 生成运行时转换的代码 */
n = (int)r;/* 也是运行时转换,没有溢出检查 */

C 及其后代语言默认不执行任何操作的算术溢出运行时检查,尽管在 C# 中可以根据需要启用此类检查。■

C and its descendants do not by default perform run-time checks for arithmetic overflow on any operation, though such checks can be enabled if desired in C#. ■

非转换类型转换

有时,特别是在系统程序中,需要更改值的类型而不更改底层实现;换句话说,将一种类型的值的位解释为另一种类型。一个常见的例子出现在内存分配算法中,该算法使用大量字节数组来表示堆,然后将该数组的部分重新解释为指针和整数(用于簿记目的),或各种用户分配的数据结构。另一个常见的例子出现在高性能数字软件中,它可能需要将浮点数重新解释为整数或记录,以便提取指数、有效数字和符号字段。这些字段可用于实现平方根、三角函数等专用算法。

Occasionally, particularly in systems programs, one needs to change the type of a value without changing the underlying implementation; in other words, to interpret the bits of a value of one type as if they were another type. One common example occurs in memory allocation algorithms, which use a large array of bytes to represent a heap, and then reinterpret portions of that array as pointers and integers (for bookkeeping purposes), or as various user-allocated data structures. Another common example occurs in high-performance numeric software, which may need to reinterpret a floating-point number as an integer or a record, in order to extract the exponent, significand, and sign fields. These fields can be used to implement special-purpose algorithms for square root, trigonometric functions, and so on.

例 7.28

Example 7.28

Ada 中未经检查的转换

Unchecked conversions in Ada

不改变底层位的类型更改称为非转换类型转换,有时也称为类型双关语。它不应与在 C 等语言中使用术语cast来表示转换。在 Ada 中,可以使用名为unchecked_conversion的内置通用子程序实例来实现非转换转换:

A change of type that does not alter the underlying bits is called a nonconverting type cast, or sometimes a type pun. It should not be confused with use of the term cast for conversions in languages like C. In Ada, nonconverting casts can be effected using instances of a built-in generic subroutine called unchecked_conversion:

-- 假设“float”已被声明为匹配 IEEE 单精度

-- assume 'float' has been declared to match IEEE single-precision

函数 cast_float_to_int 是

function cast_float_to_int is

 新的未检查转换(浮点数,整数);

 new unchecked_conversion(float, integer);

函数 cast_int_to_float 是

function cast_int_to_float is

 新的unchecked_conversion(整数,浮点数);

 new unchecked_conversion(integer, float);

将 int 转换为 float 类型

f := cast_int_to_float(n);

n := 转换为浮点型(f);

n := cast_float_to_int(f);

例 7.29

Example 7.29

C++ 中的转换和非转换强制类型转换

Conversions and nonconverting casts in C++

C++ 继承了 C 的强制转换机制,但也提供了一系列语义上更清晰的替代方法。具体来说,static_cast执行类型转换,reinterpret_cast执行非转换类型转换,dynamic_cast允许操纵多态类型指针的程序执行赋值,这些赋值的有效性无法静态保证,但可以在运行时检查(第 10 章将对此进行详细介绍)。每个方法的语法都与通用函数相同:

C++ inherits the casting mechanism of C, but also provides a family of semantically cleaner alternatives. Specifically, static_cast performs a type conversion, reinterpret_cast performs a nonconverting type cast, and dynamic_ cast allows programs that manipulate pointers ofpolymorphic types to perform assignments whose validity cannot be guaranteed statically, but can be checked at run time (more on this in Chapter 10). Syntax for each of these is that of a generic function:

设计与实现

Design & Implementation

7.6 非转换强制类型转换

7.6 Nonconverting casts

C 程序员有时会尝试通过获取对象的地址、转换结果指针的类型、然后取消引用来进行非转换类型转换(类型双关语):

C programmers sometimes attempt a nonconverting type cast (type pun) by taking the address of an object, converting the type of the resulting pointer, and then dereferencing:

r = *((浮点数*)&n);

r = *((float *) &n);

这种神秘的黑客行为通常不会产生任何运行时成本,因为大多数(但不是全部!)实现对指向整数的指针和指向浮点值的指针使用相同的表示形式 - 即地址。与号运算符(&)表示“地址”或“指向”。括号(float *)是“指向浮点数的指针”的类型名称(float 是内置浮点类型)。前缀 * 运算符是指针取消引用。整体构造导致编译器将n的位解释为浮点数。仅当n是左值(具有地址)并且整数浮点数具有相同的大小时,重新解释才会成功(再次,第二个条件在 C 语言中通常但并非总是如此)。如果 n 没有地址,则编译器将宣布静态语义错误。如果intfloat占用的字节数不同,则强制转换的效果可能取决于多种因素,包括对象的相对大小、内存的对齐和“字节顺序”(第 C-5.2 节),以及编译器对将什么放置在内存中相邻位置的选择。在 C 中,可以通过联合(变体记录)实现更安全、更可移植的非转换强制转换;我们在练习 C-8.24中考虑了此选项。

This arcane bit of hackery usually incurs no run-time cost, because most (but not all!) implementations use the same representation for pointers to integers and pointers to floating-point values—namely, an address. The ampersand operator (&) means “address of,” or “pointer to.” The parenthesized (float *) is the type name for “pointer to float” (float is a built-in floating-point type). The prefix * operator is a pointer dereference. The overall construct causes the compiler to interpret the bits of n as if it were a float. The reinterpretation will succeed only if n is an l-value (has an address), and ints and floats have the same size (again, this second condition is often but not always true in C). If n does not have an address then the compiler will announce a static semantic error. If int and float do not occupy the same number of bytes, then the effect of the cast may depend on a variety of factors, including the relative size of the objects, the alignment and “endian-ness” of memory (Section C-5.2), and the choices the compiler has made regarding what to place in adjacent locations in memory. Safer and more portable nonconverting casts can be achieved in C by means of unions (variant records); we consider this option in Exercise C-8.24.

双 d = …

double d = …

int n = static_cast<int>(d);

int n = static_cast<int>(d);

还有一个const_cast可用于删除只读限定。C++ 中的 C 风格类型转换是根据const_caststatic_castreinterpret_cast定义的;确切的行为取决于源类型和目标类型。■

There is also a const_cast that can be used to remove read-only qualification. C-style type casts in C++ are defined in terms of const_cast, static_cast, and reinterpret_cast; the precise behavior depends on the source and target types. ■

任何非转换类型转换都会对语言的类型系统造成危险的破坏。在具有弱类型系统的语言中,这种破坏可能很难发现。在具有强类型系统的语言中,使用显式非转换类型转换至少会标记代码中的危险点,从而便于在出现问题时进行调试。

Any nonconverting type cast constitutes a dangerous subversion of the language's type system. In a language with a weak type system such subversions can be difficult to find. In a language with a strong type system, the use of explicit nonconverting type casts at least labels the dangerous points in the code, facilitating debugging if problems arise.

7.2.2 类型兼容性

7.2.2 Type Compatibility

大多数语言并不要求在每种情况下类型都相等。相反,它们只是说值的类型必须与其出现的上下文的类型兼容。在赋值语句中,右侧的类型必须与左侧的类型兼容。+ 的操作数的类型必须都与支持加法的某些常见类型兼容(整数、实数,或者可能是字符串或集合)。在子程序调用中,传递到子程序中的任何参数的类型都必须与相应形式参数的类型兼容,并且传递回调用者的任何形式参数的类型都必须与相应参数的类型兼容。

Most languages do not require equivalence of types in every context. Instead, they merely say that a value's type must be compatible with that of the context in which it appears. In an assignment statement, the type of the right-hand side must be compatible with that of the left-hand side. The types of the operands of + must both be compatible with some common type that supports addition (integers, real numbers, or perhaps strings or sets). In a subroutine call, the types of any arguments passed into the subroutine must be compatible with the types of the corresponding formal parameters, and the types of any formal parameters passed back to the caller must be compatible with the types of the corresponding arguments.

类型兼容性的定义在不同语言之间差别很大。Ada 采用了相对严格的方法:且仅当 (1) ST等价、(2) 一个是另一个的子类型(或两者都是同一基类型的子类型)或 (3) 两者都是数组,且每个维度的元素数量和类型相同时, Ada 类型S与预期类型 T 兼容。Pascal 只是稍微宽松一点:除了允许混合使用基类型和子范围类型之外,它还允许在需要实数的上下文中使用整数。

The definition of type compatibility varies greatly from language to language. Ada takes a relatively restrictive approach: an Ada type S is compatible with an expected type T if and only if (1) S and T are equivalent, (2) one is a subtype of the other (or both are subtypes of the same base type), or (3) both are arrays, with the same numbers and types of elements in each dimension. Pascal was only slightly more lenient: in addition to allowing the intermixing of base and subrange types, it allowed an integer to be used in a context where a real was expected.

强迫

Coercion

每当一种语言允许在需要另一种类型的上下文中使用一种类型的值时,该语言实现必须执行自动、隐式转换为预期类型。此转换称为类型强制。与第 7.2.1 节的显式转换一样,强制可能需要运行时代码执行动态语义检查或在低级表示之间进行转换。

Whenever a language allows a value of one type to be used in a context that expects another, the language implementation must perform an automatic, implicit conversion to the expected type. This conversion is called a type coercion. Like the explicit conversions of Section 7.2.1, coercion may require run-time code to perform a dynamic semantic check or to convert between low-level representations.

例 7.30

Example 7.30

C 中的强制转换

Coercion in C

C 的类型系统相对较弱,但可以执行相当多的强制转换。它允许大多数数字类型的值在表达式中混合,并会“根据需要”来回强制转换类型。请考虑以下声明:

C, which has a relatively weak type system, performs quite a bit of coercion. It allows values of most numeric types to be intermixed in expressions, and will coerce types back and forth “as necessary.” Consider the following declarations:

短整型s;

short int s;

无符号长整型 l;

unsigned long int l;

char c; /* 可以是有符号的也可以是无符号的 —— 取决于实现 */

char c; /* may be signed or unsigned -- implementation-dependent */

float f; /* 通常是 IEEE 单精度 */

float f; /* usually IEEE single-precision */

double d; /* 通常是 IEEE 双精度 */

double d; /* usually IEEE double-precision */

假设这些变量的长度分别为 16、32、8、32 和 64 位(这在 32 位机器上很常见)。当将一种类型的变量赋值给另一种类型的变量时,强制转换可能会产生各种效果:

Suppose that these variables are 16, 32, 8, 32, and 64 bits in length, respectively—as is common on 32-bit machines. Coercion may have a variety of effects when a variable of one type is assigned into another:

s=l;/* l 的低位被解释为有符号数。 */
l=s;/* s 被符号扩展为更长的长度,然后它的位被解释为无符号数。 */
s=c;/* c 要么通过符号扩展要么通过零扩展达到 s 的长度;结果随后被解释为有符号数。*/
f=l;/* l 转换为浮点数。由于 f 的有效位较少,因此可能会丢失一些精度。*/
d=f;/* f 被转换为更长的格式;没有精度损失。 */
f=d;/* d 被转换为更短的格式;精度可能会丢失。
如果 d 的值不能用单精度表示,则结果未定义,但不是动态语义错误。*/

强制转换在语言设计中是一个颇具争议的话题。由于它允许混合使用不同类型,而程序员对此没有明确的意图,因此它严重削弱了类型的安全性。与此同时,一些设计者认为强制转换是支持抽象和程序可扩展性的自然方式,因为它使新类型与现有类型的结合使用变得更加容易。这种可扩展性论点在脚本语言(第14 章)中尤其引人注目,因为脚本语言是动态类型的,并且强调编程的简易性。大多数脚本语言都支持各种各样的强制转换,但也存在一些差异:Perl 几乎可以强制转换所有类型;而 Ruby 则要保守得多。

Coercion is a somewhat controversial subject in language design. Because it allows types to be mixed without an explicit indication of intent on the part of the programmer, it represents a significant weakening of type security. At the same time, some designers have argued that coercions are a natural way in which to support abstraction and program extensibility, by making it easier to use new types in conjunction with existing ones. This extensibility argument is particularly compelling in scripting languages (Chapter 14), which are dynamically typed and emphasize ease of programming. Most scripting languages support a wide variety of coercions, though there is some variation: Perl will coerce almost anything; Ruby is much more conservative.

在静态类型语言中,种类更多。Ada 只强制转换显式常量、子范围,以及在某些情况下强制转换具有相同类型元素的数组。Pascal 会在表达式和赋值中将整数强制转换为浮点数。Fortran 还会在赋值中将浮点值强制转换为整数,但可能会损失精度。C 会对函数的参数执行相同的强制转换。

Among statically typed languages, there is even more variety. Ada coerces nothing but explicit constants, subranges, and in certain cases arrays with the same type of elements. Pascal would coerce integers to floating-point in expressions and assignments. Fortran will also coerce floating-point values to integers in assignments, at a potential loss of precision. C will perform these same coercions on arguments to functions.

一些编译语言甚至支持对数组和记录进行强制转换。只要预期类型和实际类型具有相同的形状,Fortran 90 就允许这样做。如果两个数组具有相同数量的维数、每个维度具有相同的大小(即相同数量的元素)并且各个元素具有相同的形状,则它们具有相同的形状。如果两个记录具有相同数量的字段,并且相应的字段按顺序具有相同的形状,则它们具有相同的形状。字段名称并不重要,数组维数的实际上限和下限也不重要。Ada 的数组强制转换规则大致相当于 Fortran 90 的规则。C 不提供将整个数组作为操作数的运算。然而,C 在许多情况下允许数组和指针混合使用;我们将在第 8.5.1 节中进一步讨论这种不寻常的类型兼容性形式。Ada 和 C 都不允许记录(结构)混合使用,除非它们的类型是名称等效的。

Some compiled languages even support coercion on arrays and records. Fortran 90 permits this whenever the expected and actual types have the same shape. Two arrays have the same shape if they have the same number of dimensions, each dimension has the same size (i.e., the same number of elements), and the individual elements have the same shape. Two records have the same shape if they have the same number of fields, and corresponding fields, in order, have the same shape. Field names do not matter, nor do the actual high and low bounds of array dimensions. Ada's coercion rules for arrays are roughly equivalent to those of Fortran 90. C provides no operations that take an entire array as an operand. C does, however, allow arrays and pointers to be intermixed in many cases; we will discuss this unusual form of type compatibility further in Section 8.5.1. Neither Ada nor C allows records (structures) to be intermixed unless their types are name equivalent.

C++ 提供了静态类型语言中强制转换的最极端示例。除了一组丰富的内置规则外,C++ 还允许程序员在定义新类型(类时定义与现有类型之间的强制转换操作。应用这些操作的规则与解决重载的规则(第 3.5.2 节)以复杂的方式相互作用;它们为语言增加了很大的灵活性,但却是最难理解和正确使用的 C++ 特性之一。

C++ provides what may be the most extreme example of coercion in a statically typed language. In addition to a rich set of built-in rules, C++ allows the programmer to define coercion operations to and from existing types when defining a new type (a class). The rules for applying these operations interact in complicated ways with the rules for resolving overloading (Section 3.5.2); they add significant flexibility to the language, but are one of the most difficult C++ features to understand and use correctly.

重载和强制

Overloading and Coercion

例 7.31

Example 7.31

强制转换与加数重载

Coercion vs overloading of addends

我们已经指出(在第 3.5 节中),重载和强制(以及各种形式的多态性)有时可以达到类似的效果。这里值得详细说明一下区别。重载名称可以引用多个对象;歧义必须通过上下文解决。考虑数值量的加法。在表达式a + b中,+可以引用整数或浮点加法运算。在没有强制转换的语言中,ab必须都是整数或都是实数;编译器会根据它们的类型选择对 + 的适当解释。在具有强制转换的语言中,如果ab是实数,则 + 引用浮点加法运算;否则它引用整数加法运算。如果 a 和 b 中只有一个是实数,则另一个将被强制匹配。可以想象一种语言,其中 + 没有重载,而是在所有情况下都引用浮点加法。强制转换仍然可以允许 + 接受整数参数,但它们总是会被转换为实数。这种方法的问题在于从整数到浮点格式的转换需要花费不可忽略的时间,特别是在没有硬件转换指令的机器上,并且浮点加法比整数加法昂贵得多。■

We have noted (in Section 3.5) that overloading and coercion (as well as various forms of polymorphism) can sometimes be used to similar effect. It is worth elaborating on the distinctions here. An overloaded name can refer to more than one object; the ambiguity must be resolved by context. Consider the addition of numeric quantities. In the expression a + b, + may refer to either the integer or the floating-point addition operation. In a language without coercion, a and b must either both be integer or both be real; the compiler chooses the appropriate interpretation of + depending on their type. In a language with coercion, + refers to the floating-point addition operation if either a or b is real; otherwise it refers to the integer addition operation. If only one of a and b is real, the other is coerced to match. One could imagine a language in which + was not overloaded, but rather referred to floating-point addition in all cases. Coercion could still allow + to take integer arguments, but they would always be converted to real. The problem with this approach is that conversions from integer to floating-point format take a non-negligible amount of time, especially on machines without hardware conversion instructions, and floating-point addition is significantly more expensive than integer addition. ■

在大多数语言中,文字常量(例如数字、字符串、空集 [ [ ] ] 或空指针 [ nil ])可以在表达式中与多种类型的值混合使用。 有人可能会说常量是重载的:例如nil可能被认为是引用周围上下文中所需的任何类型的空指针值。 但更常见的是,常量只是被视为语言类型检查规则中的特例。 在内部,编译器将常量视为少数内置“常量类型”之一(int const、real const、string、null),然后根据需要将其强制转换为某种更合适的类型,即使该语言的其他地方不支持强制转换。 Ada 将这种“常量类型”概念形式化为数值:整型常量(没有小数点)被称为具有universal_integer类型;实数常量(带有嵌入小数点和/或指数的常量)的类型为universal_realuniversal_integer类型与任何整数类型兼容;universal_real与任何定点或浮点类型兼容。

In most languages, literal constants (e.g., numbers, character strings, the empty set [ [ ] ] or the null pointer [nil]) can be intermixed in expressions with values of many types. One might say that constants are overloaded: nil for example might be thought of as referring to the null pointer value for whatever type is needed in the surrounding context. More commonly, however, constants are simply treated as a special case in the language's type-checking rules. Internally, the compiler considers a constant to have one of a small number of built-in “constant types” (int const, real const, string, null), which it then coerces to some more appropriate type as necessary, even if coercions are not supported elsewhere in the language. Ada formalizes this notion of “constant type” for numeric quantities: an integer constant (one without a decimal point) is said to have type universal_integer; a real-number constant (one with an embedded decimal point and/or an exponent) is said to have type universal_real. The universal_integer type is compatible with any integer type; universal_real is compatible with any fixed-point or floating-point type.

通用引用类型

Universal Reference Types

对于系统编程,或为了方便编写保存对其他对象的引用的通用容器(集合)对象(列表、堆栈、队列、集合等),有几种语言提供了通用引用类型。在 C 和 C++ 中,这种类型称为void *。在 Clu 中,它被称为any;在 Modula-2 中,称为address;在 Modula-3 中,称为refany;在 Java 中,称为Object;在 C# 中,称为object。可以将任意左值分配给通用引用类型的对象,而不必担心类型安全:因为通用引用所引用的对象类型未知,所以编译器不允许对该对象执行任何操作。如果要维护类型安全,将值分配回特定引用类型的对象(例如,指向程序员指定的记录类型的指针)会有些棘手。例如,我们不希望将对浮点数的通用引用赋值给一个应该保存对整数的引用的变量,因为对“整数”的后续操作会错误地解释对象的位。在面向对象语言中,如何确保通用到特定赋值的有效性的问题可以推广到如何确保任何赋值的有效性的问题,其中左侧对象的类型支持右侧对象可能不支持的操作。

For systems programming, or to facilitate the writing of general-purpose container (collection) objects (lists, stacks, queues, sets, etc.) that hold references to other objects, several languages provide a universal reference type. In C and C++, this type is called void*. In Clu it is called any; in Modula-2, address; in Modula-3, refany; in Java, Object; in C#, object. Arbitrary l-values can be assigned into an object of universal reference type, with no concern about type safety: because the type of the object referred to by a universal reference is unknown, the compiler will not allow any operations to be performed on that object. Assignments back into objects of a particular reference type (e.g., a pointer to a programmer-specified record type) are a bit trickier, if type safety is to be maintained. We would not want a universal reference to a floating-point number, for example, to be assigned into a variable that is supposed to hold a reference to an integer, because subsequent operations on the “integer” would interpret the bits of the object incorrectly. In object-oriented languages, the question of how to ensure the validity of a universal-to-specific assignment generalizes to the question of how to ensure the validity of any assignment in which the type of the object on left-hand side supports operations that the object on the right-hand side may not.

确保通用到特定赋值(或者,一般来说,从不太特定到更特定的赋值)安全性的一种方法是使对象具有自描述性 - 即,在每个对象的表示中包含一个指示其类型的标签。 这种方法在面向对象语言中很常见,通常需要它进行动态方法绑定。 对象中的类型标签会占用大量空间,但允许实现防止将一种类型的对象赋值给另一种类型的变量。 在 Java 和 C# 中,通用到特定赋值需要类型转换,如果通用引用不引用转换类型的对象,则会生成异常。 在 Eiffel 中,等效操作使用特殊的赋值运算符(?=而不是:=);在 C++ 中,它使用dynamic_cast操作。

One way to ensure the safety of universal to specific assignments (or, in general, less specific to more specific assignments) is to make objects self-descriptive—that is, to include in the representation of each object a tag that indicates its type. This approach is common in object-oriented languages, which generally need it for dynamic method binding. Type tags in objects can consume a nontrivial amount of space, but allow the implementation to prevent the assignment of an object of one type into a variable of another. In Java and C#, a universal to specific assignment requires a type cast, and will generate an exception if the universal reference does not refer to an object of the casted type. In Eiffel, the equivalent operation uses a special assignment operator (?= instead of :=); in C++ it uses a dynamic_cast operation.

例 7.32

Example 7.32

Java 对象容器

Java container of Object

在 Java 和 C# 的早期版本中,程序员通常会创建容器类来保存通用引用类(分别为Objectobject )的对象。随着泛型的引入(将在7.3.1 节中讨论),这种习惯用法已经不那么常见了,但它仍然偶尔用于保存多个类的对象的容器。当从这样的容器中移除一个对象时,必须先将其(使用类型转换)分配给适当类的变量,然后才能对其进行任何有趣的操作:

In early versions of Java and C#, programmers would often create container classes that held objects of the universal reference class (Object or object, respectively). This idiom has become less common with the introduction of generics (to be discussed in Section 7.3.1), but it is still occasionally used for containers that hold objects of more than one class. When an object is removed from such a container, it must be assigned (with a type cast) into a variable of an appropriate class before anything interesting can be done with it:

import java.util.*; // 包含 Stack 容器类的库

import java.util.*; // library containing Stack container class

堆栈 myStack = new Stack();

Stack myStack = new Stack();

字符串 s = “嗨,妈妈”;

String s = “Hi, Mom”;

Foo f = new Foo(); // f 是用户定义的类类型 Foo

Foo f = new Foo(); // f is of user-defined class type Foo

myStack.推送(s);

myStack.push(s);

myStack.push(f); //我们可以将任何类型的对象推送到堆栈上

myStack.push(f); // we can push any kind of object on a stack

s = (字符串) myStack.pop();

s = (String) myStack.pop();

 // 需要类型转换,并且在运行时会产生异常

 // type cast is required, and will generate an exception at run

 // 如果堆栈顶部的元素不是字符串则时间

 // time if element at top-of-stack is not a string

在没有类型标签的语言中,无法检查将通用引用分配给特定引用类型的对象,因为对象不是自描述的:无法在运行时识别其类型。因此,程序员必须求助于(未经检查的)类型转换。

In a language without type tags, the assignment of a universal reference into an object of a specific reference type cannot be checked, because objects are not self-descriptive: there is no way to identify their type at run time. The programmer must therefore resort to an (unchecked) type conversion.

7.2.3 类型推断

7.2.3 Type Inference

我们已经了解了类型检查如何确保表达式的组成部分(例如,二元运算符的参数)具有适当的类型。但是,是什么决定了整个表达式的类型呢?在许多情况下,答案很简单。算术运算符的结果通常与操作数具有相同的类型(如果它们的类型不同,则可能在强制其中一个操作数之后)。比较的结果通常是布尔值。函数调用的结果具有在函数头中声明的类型。赋值的结果(在赋值是表达式的语言中)具有与左侧相同的类型。然而,在少数情况下,答案并不明显。例如,对子范围和复合对象的操作不一定保留操作数的类型。我们将在本小节的其余部分中研究这些情况。在下一节中,我们将考虑在 ML、Miranda 和 Haskell 中发现的一种更复杂的类型推断形式。

We have seen how type checking ensures that the components of an expression (e.g., the arguments of a binary operator) have appropriate types. But what determines the type of the overall expression? In many cases, the answer is easy. The result of an arithmetic operator usually has the same type as the operands (possibly after coercing one of them, if their types were not the same). The result of a comparison is usually Boolean. The result of a function call has the type declared in the function's header. The result of an assignment (in languages in which assignments are expressions) has the same type as the left-hand side. In a few cases, however, the answer is not obvious. Operations on subranges and composite objects, for example, do not necessarily preserve the types of the operands. We examine these cases in the remainder of this subsection. In the following section, we consider a more elaborate form of type inference found in ML, Miranda, and Haskell.

子范围和集合

Subranges and Sets

例 7.33

Example 7.33

子范围类型的推断

Inference of subrange types

对于算术运算符,当一个或多个操作数具有子范围类型时,就会出现一个简单的推理示例。例如,给出以下 Pascal 定义,

For arithmetic operators, a simple example of inference arises when one or more operands have subrange types. Given the following Pascal definitions, for example,

类型 Atype = 0..20;

type Atype = 0..20;

 B类型=10..20;

 Btype = 10..20;

var a : A类型;

var a : Atype;

 b:B类型;

 b : Btype;

a + b的类型是什么?当然它既不是Atype也不是Btype,因为可能的值范围从 10 到 40。可以想象它是一个新的匿名子范围类型,以 10 和 40 为界限。通常的答案是说,对子范围的任何算术运算的结果都具有子范围的基类型 — 在本例中为整数。

what is the type of a + b? Certainly it is neither Atype nor Btype, since the possible values range from 10 to 40. One could imagine it being a new anonymous subrange type with 10 and 40 as bounds. The usual answer is to say that the result of any arithmetic operation on a subrange has the subrange's base type—in this case, integer.

如果算术运算的结果被赋值给子范围类型的变量,则可能需要进行动态语义检查。为了避免一些不必要的检查,编译器可以在编译时跟踪每个表达式的最大和最小可能值,本质上计算匿名的10…40类型。可以使用更复杂的技术来消除循环中的许多检查;我们将在 C-17.5.2 节中考虑这些。■

If the result of an arithmetic operation is assigned into a variable of a subrange type, then a dynamic semantic check may be required. To avoid the expense of some unnecessary checks, a compiler may keep track at compile time of the largest and smallest possible values of each expression, in essence computing the anonymous 10… 40 type. More sophisticated techniques can be used to eliminate many checks in loops; we will consider these in Section C-17.5.2. ■

例 7.34

Example 7.34

集合的类型推断

Type inference for sets

操作集合时也会发生具有类型含义的运算。例如,Pascal 和 Modula 支持对离散值集合进行并集 (+)、交集 (*) 和差集 (-)。如果集合操作数的元素具有相同的基类型T ,则称集合操作数具有兼容类型。集合运算的结果为T 的集合类型。与子范围一样,编译器可以通过跟踪集合表达式的最小和最大可能成员,在某些情况下避免运行时边界检查的需要。■

Operations with type implications also occur when manipulating sets. Pascal and Modula, for example, supported union (+), intersection (*), and difference (-) on sets of discrete values. Set operands were said to have compatible types if their elements had the same base type T. The result of a set operation was then of type set of T. As with subranges, a compiler could avoid the need for run-time bounds checks in certain cases by keeping track of the minimum and maximum possible members of the set expression. ■

声明

Declarations

Ada 是第一批将for循环的索引设为新的局部变量(仅在循环中可访问)的语言之一。该语言无需程序员指定此变量的类型,而是隐式地为其分配了作为循环界限提供的表达式的基类型。

Ada was among the first languages to make the index of a for loop a new, local variable, accessible only in the loop. Rather than require the programmer to specify the type of this variable, the language implicitly assigned it the base type of the expressions provided as bounds for the loop.

例 7.35

Example 7.35

C# 中的var声明

var declarations in C#

这一思想的扩展出现在几种较新的语言中,包括 Scala、C# 3.0、C++11、Go 和 Swift,所有这些语言都允许程序员在可以根据上下文推断出声明意图的情况下从变量声明中省略类型信息。例如,在 C# 中,可以这样写

Extensions of this idea appear in several more recent languages, including Scala, C# 3.0, C++11, Go, and Swift, all of which allow the programmer to omit type information from a variable declaration when the intent of the declaration can be inferred from context. In C#, for example, one can write

var i = 123; // 等同于 int i = 123;

var i = 123; // equiv. to int i = 123;

var map = new Dictionary<string, int>(); // 等同于

var map = new Dictionary<string, int>(); // equiv. to

 // 字典<string,int> map = new 字典<string,int>();

 // Dictionary<string, int> map = new Dictionary<string, int>();

这里,赋值语句右侧的(容易确定的)类型可用于推断变量的类型,这样我们就无需显式声明它了。在 C++ 中,我们可以使用 auto 关键字实现类似的效果;在 Scala 中,我们在声明初始化变量或常量时只需省略类型名称即可。■

Here the (easily determined) type of the right-hand side of the assignment can be used to infer the variable's type, freeing us from the need to declare it explicitly. We can achieve a similar effect in C++ with the auto keyword; in Scala we simply omit the type name when declaring an initialized variable or constant. ■

例 7.36

Example 7.36

避免混乱的声明

Avoiding messy declarations

复杂声明会增加推理的便利性。例如,假设我们想要对列表的元素执行数学家所说的归约——使用某个二元函数将值“折叠在一起”。使用 C++ lambda 语法(第 3.6.4 节),我们可以写

The convenience of inference increases with complex declarations. Suppose, for example, that we want to perform what mathematicians call a reduction on the elements of a list—a “folding together” of values using some binary function. Using C++ lambda syntax (Section 3.6.4), we might write

自动减少 = [](list<int> L, int f(int, int), int s) {

auto reduce = [](list<int> L, int f(int, int), int s) {

  // s 的初始值应该是 f 的单位元素

  // the initial value of s should be the identity element for f

  对于(自动 e : L){

  for (auto e : L) {

   s = f(e,s);

   s = f(e, s);

  }

  }

  返回 s;

  return s;

 };

 };

}

}

int sum = reduce(my_list,[](int a,int b){返回 a+b;},0);

int sum = reduce(my_list, [](int a, int b){return a+b;}, 0);

int product = reduce(my_list,[](int a,int b){return a*b;},1);

int product = reduce(my_list, [](int a, int b){return a*b;}, 1);

这里,auto关键字允许我们省略原本令人望而生畏的类型指示:

Here the auto keyword allows us to omit what would have been a rather daunting indication of type:

int (*reduce) (list<int>, int (*)(int, int), int) = …

int (*reduce) (list<int>, int (*)(int, int), int) = …

  = [](list<int> L, int f(int, int), int s){…

  = [](list<int> L, int f(int, int), int s){…

例 7.37

Example 7.37

C++11 中的decltype

decltype in C++11

事实上, C++ 更进了一步,它有一个decltype关键字,可用于匹配任何现有表达式的类型。decltype 关键字在模板中特别方便,因为有时不可能提供适当的静态类型名称。例如,考虑一个通用算术包,由操作数类型AB参数化:

C++ in fact goes one step further, with a decltype keyword that can be used to match the type of any existing expression. The decltype keyword is particularly handy in templates, where it is sometimes impossible to provide an appropriate static type name. Consider, for example, a generic arithmetic package, parameterized by operand types A and B:

模板 <类型名称 A, 类型名称 B>

template <typename A, typename B>

 Aa; Bb;

 A a; B b;

 decltype(a + b)总和;

 decltype(a + b) sum;

这里sum的类型取决于C++ 强制规则下AB的类型。例如,如果AB都是int ,则sum将是int 。如果AB之一是double,另一个是int,则sum将是double。使用适当的(用户提供的)强制规则,sum可能被推断为具有复数(实数 + 虚数)或任意精度(“bignum”)类型。■

Here the type of sum depends on the types of A and B under the C++ coercion rules. If A and B are both int, for example, then sum will be an int. If one of A and B is double and the other is int, then sum will be a double. With appropriate (user-provided) coercion rules, sum might be inferred to have a complex (real + imaginary) or arbitrary-precision (“bignum”) type. ■

7.2.4 机器学习中的类型检查

7.2.4 Type Checking in ML

类型推断的最复杂形式出现在 ML 系列函数式语言中,包括 Haskell、F# 以及 ML 本身的 OCaml 和 SML 方言。程序员可以选择在这些语言中声明对象的类型,在这种情况下,编译器的行为与更传统的静态类型语言非常相似。然而,正如我们在第7.1 节开头提到的那样,程序员也可以选择不声明某些类型,在这种情况下,编译器将根据已知的文字常量类型、具有它们的任何对象的显式声明类型以及语法结构推断它们程序。ML 样式类型推断是该语言的创建者 Robin Milner 的发明。4

The most sophisticated form of type inference occurs in the ML family of functional languages, including Haskell, F#, and the OCaml and SML dialects of ML itself. Programmers have the option of declaring the types of objects in these languages, in which case the compiler behaves much like that of a more traditional statically typed language. As we noted near the beginning of Section 7.1, however, programmers may also choose not to declare certain types, in which case the compiler will infer them, based on the known types of literal constants, the explicitly declared types of any objects that have them, and the syntactic structure of the program. ML-style type inference is the invention of the language's creator, Robin Milner.4

推理机制的关键是,当类型系统的规则规定两个表达式的类型必须相同时,统一它们的(部分)类型信息。已知的彼此信息也会知道对方的信息。任何发现的不一致之处都被标识为静态语义错误。任何在推理后类型仍未完全指定的表达式都会自动成为多态的;这就是第7.1.2 节中提到的隐式参数多态性。ML 系列语言还包含强大的运行时模式匹配功能和几种非常规的结构化类型,包括有序元组、(无序)记录、列表、包含联合和递归类型的数据类型机制,以及具有继承(类型扩展)和显式参数多态性(泛型)的丰富模块系统。我们将在第11.4 节中更详细地讨论 ML 类型。

The key to the inference mechanism is to unify the (partial) type information available for two expressions whenever the rules of the type system say that their types must be the same. Information known about each is then known about the other as well. Any discovered inconsistencies are identified as static semantic errors. Any expression whose type remains incompletely specified after inference is automatically polymorphic; this is the implicit parametric polymorphism referred to in Section 7.1.2. ML family languages also incorporate a powerful run-time pattern-matching facility and several unconventional structured types, including ordered tuples, (unordered) records, lists, a datatype mechanism that subsumes unions and recursive types, and a rich module system with inheritance (type extension) and explicit parametric polymorphism (generics). We will consider ML types in more detail in Section 11.4.

例 7.38

Example 7.38

Ocaml 中的斐波那契函数

Fibonacci function in Ocaml

以下是示例 6.87中介绍的尾递归斐波那契函数的 OCaml 版本:

The following is an OCaml version of the tail-recursive Fibonacci function introduced in Example 6.87:

1. 设 fib n =

1. let fib n =

2. 让 rec fib_helper n1 n2 i =

2.   let rec fib_helper n1 n2 i =

3. 如果 i = n 则 n2

3.    if i = n then n2

4. else fib_helper n2 (n1 + n2) (i + 1) 在

4.    else fib_helper n2 (n1 + n2) (i + 1) in

5. fib_helper 0 10;;

5.   fib_helper 0 10;;

内部let结构引入了嵌套作用域:函数fib_helper嵌套在fib中。外部函数fib的主体是表达式fib_helper 0 10 。 fib_helper的主体是一个if…then…else表达式;它计算结果为n2fib_helper n2 (n1 + n2) (i + 1),具体取决于fib_helper的第三个参数是否为 n。关键字 rec 表示fib_helper是递归的,因此它的名称应该在其自己的主体中可用 - 而不仅仅是在let的主体中。

The inner let construct introduces a nested scope: function fib_helper is nested inside fib. The body of the outer function, fib, is the expression fib_helper 0 10. The body of fib_helper is an if… then …else expression; it evaluates to either n2 or to fib_helper n2 (n1 + n2) (i + 1), depending on whether the third argument to fib_helper is n or not. The keyword rec indicates that fib_helper is recursive, so its name should be made available within its own body—not just in the body of the let.

给定此函数定义,OCaml 编译器将粗略地进行如下推理:fib_helper的参数i必须为int类型,因为它在第 4 行与 1 相加。类似地,fib的参数n必须为int类型,因为它在第 3 行与i进行了比较。在第 5 行对fib_helper的调用中,所有三个参数的类型都是int,并且由于这是唯一的调用,所以n1n2的类型为int 。此外, i的类型与之前的推断一致,即int,并且第 4 行递归调用的参数类型也同样一致。由于fib_helper在第 3 行返回n2,因此第 5 行调用的结果将为 int。由于fib立即将此结果作为其自身的结果返回,因此fib的返回类型为int。■

Given this function definition, an OCaml compiler will reason roughly as follows: Parameter i of fib_helper must have type int, because it is added to 1 at line 4. Similarly, parameter n of fib must have type int, because it is compared to i at line 3. In the call to fib_helper at line 5, the types of all three arguments are int, and since this is the only call, the types of n1 and n2 are int. Moreover the type of i is consistent with the earlier inference, namely int, and the types of the arguments to the recursive call at line 4 are similarly consistent. Since fib_helper returns n2 at line 3, the result of the call at line 5 will be an int. Since fib immediately returns this result as its own result, the return type of fib is int. ■

例 7.39

Example 7.39

使用显式类型检查

Checking with explicit types

当然,如果我们的任何函数或参数都用显式类型声明,那么它们将与所有其他证据进行一致性检查。例如,我们可能从

Of course, if any of our functions or parameters had been declared with explicit types, these would have been checked for consistency with all the other evidence. We might, for example, have begun with

让 fib (n : int) : int = …

let fib (n : int) : int = …

表示函数的参数和返回值都应该是整数。从某种意义上说,OCaml 中的显式类型声明充当了编译器检查的文档。■

to indicate that the function's parameter and return value were both expected to be integers. In a sense, explicit type declarations in OCaml serve as compiler-checked documentation. ■

例 7.40

Example 7.40

表达式类型

Expression types

因为 OCaml 是一种函数式语言,所以每个结构都是一个表达式。编译器会为每个对象和每个表达式推断出一个类型。因为函数是一等值,所以它们也有类型。上面的 fib 的类型int - > int;也就是说,它是一个从整数到整数的函数。fib_helper 的类型是int -> int -> int -> int;也就是说,它是一个接受三个整数参数并产生一个整数结果的函数。请注意,在多参数函数的声明和调用中,括号通常都会被省略。如果我们说

Because OCaml is a functional language, every construct is an expression. The compiler infers a type for every object and every expression. Because functions are first-class values, they too have types. The type of fib above is int -> int; that is, a function from integers to integers. The type of fib_helper is int -> int -> int -> int; that is, a function that takes three integer arguments and produces an integer result. Note that parentheses are generally omitted in both declarations of and calls to multiargument functions. If we had said

让 rec fib_helper (n1, n2, i) =

let rec fib_helper (n1, n2, i) =

 如果 i = n 则 n2

 if i = n then n2

 否则 fib_helper (n2,n1+n2,i+1) 在…

 else fib_helper (n2, n1+n2, i+1) in …

那么fib_helper就会接受一个表达式(一个三元素元组)作为参数。5

then fib_helper would have accepted a single expression—a three-element tuple—as argument.5

例 7.41

Example 7.41

类型不一致

Type inconsistency

ML 系列中的类型正确性相当于我们所谓的类型一致性:如果类型检查算法可以为每个表达式推断出唯一的类型,并且没有矛盾,也没有出现重载名称的歧义,则程序是类型正确的。如果程序员使用对象不一致,编译器就会报错。在包含以下定义的程序中,

Type correctness in the ML family amounts to what we might call type consistency: a program is type correct if the type checking algorithm can reason out a unique type for every expression, with no contradictions and no ambiguous occurrences of overloaded names. If the programmer uses an object inconsistently, the compiler will complain. In a program containing the following definition,

设圆 r = r *. 2.0 *. 3.14159;;

let circum r = r *. 2.0 *. 3.14159;;

编译器将推断出circum的参数是float类型,因为它与浮点常数2.03.14159组合,使用浮点乘法运算符 *.(此处的点是运算符名称的一部分;有一个单独的整数乘法运算符 *)。如果我们尝试将circum应用于整数参数,编译器将生成类型冲突错误消息。■

the compiler will infer that circum's parameter is of type float, because it is combined with the floating-point constants 2.0 and 3.14159, using *., the floating-point multiplication operator (here the dot is part of the operator name; there is a separate integer multiplication operator, *). If we attempt to apply circum to an integer argument, the compiler will produce a type clash error message. ■

尽管该语言通常在生产环境中编译,但标准 OCaml 发行版还包含一个交互式解释器。程序员可以“在线”与解释器交互,每次输入一行。解释器会逐步处理这些输入,为每个源代码函数生成中间表示,并生成任何适当的静态错误消息。这种交互方式模糊了解释和编译之间的传统区别。虽然语言实现在程序执行期间保持活动状态,但它会在评估给定的程序片段之前执行所有可能的语义检查(生产编译器会检查的所有内容)。

Though the language is usually compiled in production environments, the standard OCaml distribution also includes an interactive interpreter. The programmer can interact with the interpreter “on line,” giving it input a line at a time. The interpreter processes this input incrementally, generating an intermediate representation for each source code function, and producing any appropriate static error messages. This style of interaction blurs the traditional distinction between interpretation and compilation. While the language implementation remains active during program execution, it performs all possible semantic checks—everything that the production compiler would check—before evaluating a given program fragment.

例 7.42

Example 7.42

多态函数

Polymorphic functions

与程序员必须显式声明所有类型的语言相比,ML 系列语言的类型推断具有简洁和方便交互使用的优势。更重要的是,它几乎免费提供了一种强大的隐式参数多态性形式。虽然 OCaml 程序中所有对象的使用都必须一致,但它们不必完全指定。考虑图 7.1中所示的 OCaml 函数。这里的相等性测试 (=) 是一个内置的多态函数,类型为'a -> 'a -> bool;也就是说,该函数采用两个相同类型的参数并产生布尔结果。标记 ' a称为类型变量;它代表任何类型,并隐式地充当泛型构造中的显式类型参数的角色(第 7.3.1 节10.1.1节)。对 = 的给定调用中,每个 ' a实例都必须表示相同类型,但不同调用中 a 的实例可以不同。从 = 的类型开始,OCaml 编译器可以推断出compare的类型是'a -> 'a -> 'a -> string 。因此 compare 是多态的;它不依赖于xpq的类型,只要它们都相同。要注意的关键点是程序员不必做任何特殊的事情来使compare具有多态性:多态性是 ML 风格类型推断的自然结果。■

In comparison to languages in which programmers must declare all types explicitly, the type inference of ML-family languages has the advantage of brevity and convenience for interactive use. More important, it provides a powerful form of implicit parametric polymorphism more or less for free. While all uses of objects in an OCaml program must be consistent, they do not have to be completely specified. Consider the OCaml function shown in Figure 7.1. Here the equality test (=) is a built-in polymorphic function of type 'a -> 'a -> bool; that is, a function that takes two arguments of the same type and produces a Boolean result. The token 'a is called a type variable; it stands for any type, and takes, implicitly, the role of an explicit type parameter in a generic construct (Sections 7.3.1 and 10.1.1). Every instance of 'a in a given call to = must represent the same type, but instances of a in different calls can be different. Starting with the type of =, an OCaml compiler can reason that the type of compare is 'a -> 'a -> 'a -> string. Thus compare is polymorphic; it does not depend on the types of x, p, and q, so long as they are all the same. The key point to observe is that the programmer did not have to do anything special to make compare polymorphic: polymorphism is a natural consequence of ML-style type inference. ■

编号07-01-9780124104099
图 7.1一个 OCaml 程序用于说明类型一致性检查。

设计与实现

Design & Implementation

7.7 Haskell 中重载函数的类型类

7.7 Type classes for overloaded functions in Haskell

在图 7.1的 OCaml 代码中,参数xpq必须支持相等运算符 (=)。OCaml 通过允许对任何内容进行相等比较,然后在运行时进行检查以确保比较确实有意义,从而使此操作变得简单。例如,尝试比较两个函数将导致运行时错误。这很不幸,因为 OCaml(以及其他 ML 系列语言)中的大多数其他类型检查都可以在编译时进行。类似地,OCaml 为几乎所有类型提供了内置的排序定义(<、>、<= 和 >=),即使它没有意义,以便程序员可以创建需要它的多态函数,如minmaxsort 。像average这样的函数可能以多态方式适用于所有数字类型(可能对整数进行四舍五入),但它无法在 OCaml 中定义:每种数字类型都有自己的加法和除法运算;没有运算符重载。

In the OCaml code of Figure 7.1, parameters x, p, and q must support the equality operator (=). OCaml makes this easy by allowing anything to be compared for equality, and then checking at run time to make sure that the comparison actually makes sense. An attempt to compare two functions, for example, will result in a run-time error. This is unfortunate, given that most other type checking in OCaml (and in other ML-family languages) can happen at compile time. In a similar vein, OCaml provides a built-in definition of ordering (<, >, <=, and >=) on almost all types, even when it doesn't make sense, so that the programmer can create polymorphic functions like min, max, and sort, which require it. A function like average, which might plausibly work in a polymorphic fashion for all numeric types (presumably with roundoff for integers) cannot be defined in OCaml: each numeric type has its own addition and division operations; there is no operator overloading.

Haskell 使用类型类的机制克服了这些限制。如示例 3.28中所述,这些明确标识了支持特定重载函数或函数集的类型。例如,Ord 类中任何类型的元素支持 <、>、<= 和 >= 运算。Enum 类中任何类型的元素都是可数的;Num类型支持加、减和乘法;FractionalReal类型还支持除法。在图 7.1中代码的 Haskell 等效版本中,参数xp和 q 将被推断为属于​​ Eq类中的某种类型。传递给sort的数组的元素将被推断为属于​​ Ord类中的某种类型。因此,Haskell 中的类型一致性可以在编译时完全验证:无需运行时检查。

Haskell overcomes these limitations using the machinery of type classes. As mentioned in Example 3.28, these explicitly identify the types that support a particular overloaded function or set of functions. Elements of any type in the Ord class, for example, support the <, >, <=, and >= operations. Elements of any type in the Enum class are countable; Num types support addition, subtraction, and multiplication; Fractional and Real types additionally support division. In the Haskell equivalent of the code in Figure 7.1, parameters x, p, and q would be inferred to belong to some type in the class Eq. Elements of an array passed to sort would be inferred to belong to some type in the class Ord. Type consistency in Haskell can thus be verified entirely at compile time: there is no need for run-time checks.

类型检查

Type Checking

OCaml 编译器根据一组明确定义的约束来验证类型一致性。例如,

An OCaml compiler verifies type consistency with respect to a well-defined set of constraints. For example,

 所有出现的相同标识符(遵守范围规则)都具有相同的类型。

 All occurrences of the same identifier (subject to scope rules) have the same type.

 if…then…else表达式中,条件为bool类型,并且thenelse子句具有相同的类型。

 In an if… then … else expression, the condition is of type bool, and the then and else clauses have the same type.

 程序员定义的函数具有类型'a -> 'b -> …-> 'r,其中'a'b等是该函数参数的类型,'r是其结果(构成其主体的表达式)的类型。

 A programmer-defined function has type 'a -> 'b -> …-> 'r, where 'a, 'b, and so forth are the types of the function's parameters, and 'r is the type of its result (the expression that forms its body).

 当应用(调用)函数时,传递的实参的类型与函数定义中的形参的类型相同。应用(即调用构成的表达式)的类型与函数定义中结果的类型相同。

 When a function is applied (called), the types of the arguments that are passed are the same as the types of the parameters in the function's definition. The type of the application (i.e., the expression constituted by the call) is the same as the type of the result in the function's definition.

例 7.43

Example 7.43

统一的一个简单例子

A simple instance of unification

在任何要求两个类型AB是“相同”的情况下,OCaml 编译器必须统一它所知道的关于AB的知识,以生成它们共同类型的(可能更详细的)描述。推断可以朝任何一个方向进行,也可以同时朝两个方向进行。例如,如果编译器确定E1是'a * int类型的表达式(即,已知第二个元素是整数的两元素元组),而E2是 string * 'b类型的表达式,那么在表达式 if x then E1 else E2中,它可以推断出'astring'bint。因此x的类型为bool,而E1E2的类型为 string * int。■

In any case where two types A and B are required to be “the same,” the OCaml compiler must unify what it knows about A and B to produce a (potentially more detailed) description of their common type. The inference can work in either direction, or both directions at once. For example, if the compiler has determined that E1 is an expression of type 'a * int (that is, a two-element tuple whose second element is known to be an integer), and that E2 is an expression of type string * 'b, then in the expression if x then E1 else E2, it can infer that 'a is string and 'b is int. Thus x is of type bool, and E1 and E2 are of type string * int. ■

设计与实现

Design & Implementation

7.8 统一

7.8 Unification

统一是一种强大的技术。除了在类型推断中的作用(也出现在 C++ 的模板 [泛型] 中)之外,统一在 Prolog 和其他逻辑语言的计算模型中也起着核心作用。我们将在第12.1 节中讨论后者的作用。在一般情况下,统一两个表达式类型的成本可能是指数级的 [ Mai90 ],但实践中往往不会出现病态情况。

Unification is a powerful technique. In addition to its role in type inference (which also arises in the templates [generics] of C++), unification plays a central role in the computational model of Prolog and other logic languages. We will consider this latter role in Section 12.1. In the general case the cost of unifying the types of two expressions can be exponential [Mai90], but the pathological cases tend not to arise in practice.

在07-01-9780124104099检查你的理解

Check Your Understanding

13. 类型等价类型兼容有什么区别?

13. What is the difference between type equivalence and type compatibility?

14.讨论 结构等价名称等价类型的比较优势。说出三种分别使用这两种方法的语言。

14. Discuss the comparative advantages of structural and name equivalence for types. Name three languages that use each approach.

15.解释 严格名称等价与松散名称等价之间的区别。

15. Explain the difference between strict and loose name equivalence.

16.解释 Ada 中派生类型和子类型之间的区别。

16. Explain the distinction between derived types and subtypes in Ada.

17.解释 类型转换类型强制非转换类型强制之间的区别。

17. Explain the differences among type conversion, type coercion, and nonconverting type casts.

18. 总结支持和反对强制的论点。

18. Summarize the arguments for and against coercion.

19. 在什么情况下类型转换需要运行时检查?

19. Under what circumstances does a type conversion require a run-time check?

20. 通用引用类型有何用途?

20. What purpose is served by universal reference types?

21. 什么是类型推断?描述它发生的三种情况。

21. What is type inference? Describe three contexts in which it occurs.

22. 在什么情况下 ML 编译器会宣布类型冲突?

22. Under what circumstances does an ML compiler announce a type clash?

23. 解释ML的类型推断如何自然地导致多态性。

23. Explain how the type inference of ML leads naturally to polymorphism.

24. 为什么机器学习程序员经常声明变量的类型,即使他们不需要这样做?

24. Why do ML programmers often declare the types of variables, even when they don't have to?

25. 什么是统一?它在机器学习中的作用是什么?

25. What is unification? What is its role in ML?

7.3 参数多态性

7.3 Parametric Polymorphism

例 7.44

Example 7.44

在 OCaml 或 Haskell 中查找最小值

Finding the minimum in OCaml or Haskell

正如我们在上一节中看到的,ML 系列语言中的函数自然是多态的。考虑一个简单的任务,即找到两个值中的最小值。在 OCaml 中,函数

As we have seen in the previous section, functions in ML-family languages are naturally polymorphic. Consider the simple task of finding the minimum of two values. In OCaml, the function

让最小 xy = 如果 x < y 则 x 否则 y;;

let min x y = if x < y then x else y;;

可以应用于任何类型的参数,尽管有时内置的 < 定义可能不是程序员想要的。在 Haskell 中,相同的函数(减去尾随的分号)可以应用于 Ord 类中任何类型的参数程序员可以通过提供 < 的定义向此类添加新类型。复杂的类型推断允许编译器在 OCaml 中在编译时执行大多数检查,在 Haskell 中则执行所有检查(有关详细信息,请参阅边栏 7.7)。

can be applied to arguments of any type, though sometimes the built-in definition of < may not be what the programmer would like. In Haskell the same function (minus the trailing semicolons) could be applied to arguments of any type in the class Ord; the programmer could add new types to this class by providing a definition of <. Sophisticated type inference allows the compiler to perform most checking at compile time in OCaml, and all of it in Haskell (see Sidebar 7.7 for details).

在 OCaml 中,我们的min函数的类型为'a -> 'a -> ' a;在 Haskell 中,类型为Ord a => a -> a -> a。虽然 min 的显式参数是xy,但我们可以将a视为额外的隐式参数——类型参数。因此,ML 系列语言被认为提供隐式参数多态性。■

In OCaml, our min function would be said to have type 'a -> 'a -> ' a; in Haskell, it would be Ord a => a -> a -> a. While the explicit parameters of min are x and y, we can think of a as an extra, implicit parameter—a type parameter. For this reason, ML-family languages are said to provide implicit parametric polymorphism. ■

例 7.45

Example 7.45

Scheme 中的隐式多态性

Implicit polymorphism in Scheme

如果我们愿意将类型检查延迟到运行时,没有编译时类型推断的语言也可以提供类似的便利性和表现力。在 Scheme 中,我们的 min 函数可以这样写:

Languages without compile-time type inference can provide similar convenience and expressiveness, if we are willing to delay type checking until run time. In Scheme, our min function would be written like this:

(定义最小值(lambda(ab)(如果(<ab)ab)))

(define min (lambda (a b) (if (< a b) a b)))

与 OCaml 或 Haskell 一样,它没有提及类型。典型的 Scheme 实现使用一个解释器来检查 min 的参数,并在运行时确定它们是否互相兼容并支持 < 运算符。根据上述定义,表达式 ( min 123 456 ) 的计算结果为123(min 3.14159 2.71828)的计算结果为2.71828。表达式 ( min “abc” “def” ) 在求值时会产生运行时错误,因为字符串比较运算符名为string<?,而不是<。■

As in OCaml or Haskell, it makes no mention of types. The typical Scheme implementation employs an interpreter that examines the arguments to min and determines, at run time, whether they are mutually compatible and support a < operator. Given the definition above, the expression (min 123 456) evaluates to 123; (min 3.14159 2.71828) evaluates to 2.71828. The expression (min “abc” “def”) produces a run-time error when evaluated, because the string comparison operator is named string<?, not <. ■

例 7.46

Example 7.46

Ruby 中的鸭子类型

Duck typing in Ruby

Smalltalk 率先在面向对象语言中引入了类似的运行时检查,Objective C、Swift、Python 和 Ruby 等也采用了这种检查。在这些语言中,如果对象支持当前调用的任何方法,则假定该对象具有可接受的类型。例如,在 Ruby 中,min是集合类支持的预定义方法。假设集合C的元素支持比较(<=>运算符),C.min将返回最小元素:

Similar run-time checks for object-oriented languages were pioneered by Smalltalk, and appear in Objective C, Swift, Python, and Ruby, among others. In these languages, an object is assumed to have an acceptable type if it supports whatever method is currently being invoked. In Ruby, for example, min is a predefined method supported by collection classes. Assuming that the elements of collection C support a comparison (<=> operator), C.min will return the minimum element:

[5, 9, 3, 6].min#3(数组)

[5, 9, 3, 6].min # 3 (array)

(2..10).分钟#2(范围)

(2..10).min # 2 (range)

[“apple”, “pear”, “orange”].min # “apple” (字典顺序)

[“apple”, “pear”, “orange”].min # “apple” (lexicographic order)

[“苹果”, “梨”, “橙子”].min {

[“apple”, “pear”, “orange”].min {

 |a,b| a.长度 <=> b.长度

 |a,b| a.length <=> b.length

} #“梨”

} # “pear”

对于对min的最终调用,我们以尾随块的形式提供了比较运算符的替代定义。■

For the final call to min, we have provided, as a trailing block, an alternative definition of the comparison operator. ■

这种检查操作风格(如果对象支持请求的方法,则该对象具有可接受的类型)有时称为鸭子类型。它的名字来源于“如果它走路像鸭子,叫声像鸭子,那么它一定是鸭子。” 6

This operational style of checking (an object has an acceptable type if it supports the requested method) is sometimes known as duck typing. It takes its name from the notion that “if it walks like a duck and quacks like a duck, then it must be a duck.”6

7.3.1 通用子程序和类

7.3.1 Generic Subroutines and Classes

Scheme、Smalltalk、Ruby 等语言中的多态性缺点是需要进行运行时检查,这会产生不小的成本,并延迟错误报告。ML 系列语言的隐式多态性避免了这些缺点,但需要高级类型推断。对于其他编译语言,显式参数多态性(也称为泛型)允许程序员在声明子例程或类时指定类型参数。然后,编译器在静态类型检查过程中使用这些参数。

The disadvantage of polymorphism in Scheme, Smalltalk, Ruby, and the like is the need for run-time checking, which incurs nontrivial costs, and delays the reporting of errors. The implicit polymorphism of ML-family languages avoids these disadvantages, but requires advanced type inference. For other compiled languages, explicit parametric polymorphism (otherwise known as generics) allows the programmer to specify type parameters when declaring a subroutine or class. The compiler then uses these parameters in the course of static type checking.

例 7.47

Example 7.47

Ada 中的通用min函数

Generic min function in Ada

提供泛型的语言包括 Ada、C++(称之为模板)、Eiffel、Java、C# 和 Scala。作为一个具体的例子,考虑图 7.2左侧的重载min函数。这里整数和浮点版本仅在参数和返回值的类型上有所不同。我们可以利用这种相似性来定义一个版本,它不仅适用于整数和实数,而且适用于任何值完全有序的类型。此代码出现在图 7.2的右侧。min的初始(无主体)声明前面有一个泛型子句,指定创建最小函数的具体实例需要两件事:类型T和相应的比较例程。此声明后面是min的实际代码,以及此代码的整数和浮点类型的实例。给定适当的比较例程(未显示),我们还可以实例化stringdate等类型的版本,如最后两行所示。 ( string_min定义中提到的“<”操作可能被重载了;编译器通过查找接受类型T参数的“<”版本来解决重载问题,其中已知T字符串。)■

Languages that provide generics include Ada, C++ (which calls them templates), Eiffel, Java, C#, and Scala. As a concrete example, consider the overloaded min functions on the left side of Figure 7.2. Here the integer and floating-point versions differ only in the types of the parameters and return value. We can exploit this similarity to define a single version that works not only for integers and reals, but for any type whose values are totally ordered. This code appears on the right side of Figure 7.2. The initial (bodyless) declaration of min is preceded by a generic clause specifying that two things are required in order to create a concrete instance of a minimum function: a type, T, and a corresponding comparison routine. This declaration is followed by the actual code for min, and instantiations of this code for integer and floating-point types. Given appropriate comparison routines (not shown), we can also instantiate versions for types like string and date, as shown on the last two lines. (The “<” operation mentioned in the definition of string_min is presumably overloaded; the compiler resolves the overloading by finding the version of “<” that takes arguments of type T, where T is already known to be string.) ■

编号:F07-02-9780124104099
图 7.2 Ada 中的重载()与泛型(右)。

例 7.48

Example 7.48

C++ 中的通用队列

Generic queues in C++

在面向对象语言中,泛型最常用于参数化整个类。除其他功能外,此类类可以用作容器- 数据抽象,其实例包含其他对象的集合,但其操作通常与所包含对象的类型无关。容器的示例包括堆栈、队列、堆、集合和字典(映射)抽象,它们以列表、数组、树或哈希表的形式实现。在没有泛型的情况下,某些语言(C 是一个明显的例子,Java 和 C# 的早期版本也是如此)可以定义对任意对象的引用队列,但使用这样的队列需要放弃编译时检查的类型转换(练习 7.8 )。图 7.3显示了 C++ 中的简单泛型队列。■

In an object-oriented language, generics are most often used to parameterize entire classes. Among other things, such classes may serve as containers—data abstractions whose instances hold a collection of other objects, but whose operations are generally oblivious to the type of the objects they contain. Examples of containers include stack, queue, heap, set, and dictionary (mapping) abstractions, implemented as lists, arrays, trees, or hash tables. In the absence of generics, it is possible in some languages (C is an obvious example, as were early versions of Java and C#) to define a queue of references to arbitrary objects, but use of such a queue requires type casts that abandon compile-time checking (Exercise 7.8). A simple generic queue in C++ appears in Figure 7.3. ■

编号07-03-9780124104099
图 7.3 C++ 中的基于通用数组的队列。

例 7.49

Example 7.49

通用参数

Generic parameters

我们可以将泛型参数视为支持编译时自定义,允许编译器创建参数化子例程或类的适当版本。在某些语言(例如 Java 和 C#)中,泛型参数必须始终是类型。其他语言则更通用。例如,在 Ada 和 C++ 中,泛型也可以通过值进行参数化。我们可以在图 7.3中看到一个例子,其中使用了一个整数参数来指定队列的最大长度。在 C++ 中,该值必须是编译时常量;在支持动态大小数组(第 8.2.2 节)的 Ada 中,其评估可以延迟到详细说明时进行。■

We can think of generic parameters as supporting compile-time customization, allowing the compiler to create an appropriate version of the parameterized subroutine or class. In some languages—Java and C#, for example—generic parameters must always be types. Other languages are more general. In Ada and C++, for example, a generic can be parameterized by values as well. We can see an example in Figure 7.3, where an integer parameter has been used to specify the maximum length of the queue. In C++, this value must be a compile-time constant; in Ada, which supports dynamic-size arrays (Section 8.2.2), its evaluation can be delayed until elaboration time. ■

实现选项

Implementation Options

泛型可以通过多种方式实现。在 Ada 和 C++ 的大多数实现中,它们是一种纯静态机制:创建和使用泛型代码的多个实例所需的所有工作都在编译时进行。通常情况下,编译器会为每个实例创建一份单独的代码副本。(C++更进一步,并安排对这些实例中的每一个进行独立类型检查。)如果使用同一组参数实例化多个队列,则编译器可能会在它们之间共享入队和出队例程的代码。如果两种类型恰好具有相同的大小,那么聪明的编译器可能会安排将整数队列的代码与浮点数队列的代码共享,但这种优化不是必需的,如果没有发生这种情况,程序员也不应该感到惊讶。

Generics can be implemented several ways. In most implementations of Ada and C++ they are a purely static mechanism: all the work required to create and use multiple instances of the generic code takes place at compile time. In the usual case, the compiler creates a separate copy of the code for every instance. (C++ goes farther, and arranges to type-check each of these instances independently.) If several queues are instantiated with the same set of arguments, then the compiler may share the code of the enqueue and dequeue routines among them. A clever compiler may arrange to share the code for a queue of integers with the code for a queue of floating-point numbers, if the two types happen to have the same size, but this sort of optimization is not required, and the programmer should not be surprised if it doesn't occur.

设计与实现

Design & Implementation

7.9 机器学习中的泛型

7.9 Generics in ML

也许令人惊讶的是,尽管类型推断“免费”提供了隐式多态性,但 OCaml 和 SML 都提供了显式多态性(泛型),其形式是称为函子的参数化模块。与隐式多态性不同,函子允许 OCaml 或 SML 程序员指示函数集合和其他值(即模块的内容)共享一组通用的泛型参数。然后由编译器强制执行此共享。此外,函子调用(泛型实例化)导出的任何类型都保证是不同的,即使它们的签名(接口)相同。与 Ada 和 C++ 一样,ML 中的泛型参数可以是值,也可以是类型。

Perhaps surprisingly, given the implicit polymorphism that comes “for free” with type inference, both OCaml and SML provide explicit polymorphism—generics—as well, in the form of parameterized modules called functors. Unlike the implicit polymorphism, functors allow the OCaml or SML programmer to indicate that a collection of functions and other values (i.e., the contents of a module) share a common set of generic parameters. This sharing is then enforced by the compiler. Moreover, any types exported by a functor invocation (generic instantiation) are guaranteed to be distinct, even though their signatures (interfaces) are the same. As in Ada and C++, generic parameters in ML can be values as well as types.

注意:虽然 Haskell 也提供了一种称为Functor的东西(具体来说,是一种支持映射函数的类型类),但其对该术语的使用与 OCaml 和 SML 的用法几乎没有共同之处。

NB: While Haskell also provides something called a Functor (specifically, a type class that supports a mapping function), its use of the term has little in common with that ofOCaml and SML.

相比之下,Java 保证给定泛型的所有实例在运行时共享相同的代码。实际上,如果T是 Java 中的泛型类型参数,则类T的对象将被视为标准基类Object的实例,只是程序员不必插入显式强制类型转换即可将它们用作类T的对象,并且编译器静态保证省略的强制类型转换永远不会失败。C# 走了一条折衷路线。与 C++ 一样,它将为不同的原始类型或值类型创建泛型的专门实现。但是,与 Java 一样,它要求泛型代码本身明显是类型安全的,与任何特定实例中提供的参数无关。我们将在C-7.3.2 节中更详细地研究 C++、Java 和 C# 泛型之间的权衡。

Java, by contrast, guarantees that all instances of a given generic will share the same code at run time. In effect, if T is a generic type parameter in Java, then objects of class T are treated as instances of the standard base class Object, except that the programmer does not have to insert explicit casts to use them as objects of class T, and the compiler guarantees, statically, that the elided casts will never fail. C# plots an intermediate course. Like C++, it will create specialized implementations of a generic for different primitive or value types. Like Java, however, it requires that the generic code itself be demonstrably type safe, independent of the arguments provided in any particular instantiation. We will examine the tradeoffs among C++, Java, and C# generics in more detail in Section C-7.3.2.

通用参数约束

Generic Parameter Constraints

因为泛型是一种抽象,所以它的接口(声明的标题)提供抽象用户必须知道的所有信息非常重要。包括 Ada、Java、C#、Scala、OCaml 和 SML 在内的多种语言都支持这种接口。尝试通过约束泛型参数来强制执行此规则。具体来说,它们要求显式声明允许对泛型参数类型执行的操作。

Because a generic is an abstraction, it is important that its interface (the header of its declaration) provide all the information that must be known by a user of the abstraction. Several languages, including Ada, Java, C#, Scala, OCaml, and SML, attempt to enforce this rule by constraining generic parameters. Specifically, they require that the operations permitted on a generic parameter type be explicitly declared.

例 7.50

Example 7.50

使用Ada 中的约束

with constraints in Ada

在 Ada 中,程序员可以通过尾随 with 子句指定泛型类型参数的操作。我们在图 7.2(右侧)的“minimum”函数中看到了一个简单的示例。Ada 中泛型排序例程的声明可能类似:

In Ada, the programmer can specify the operations of a generic type parameter by means of a trailing with clause. We saw a simple example in the “minimum” function of Figure 7.2 (right side). The declaration of a generic sorting routine in Ada might be similar:

通用的

generic

 类型 T 是私有的;

 type T is private;

 类型 T_array 是 T 的数组(整数范围 <>);

 type T_array is array (integer range <>) of T;

 使用函数“<”(a1, a2 : T)返回布尔值;

 with function “<”(a1, a2 : T) return boolean;

程序排序(A:输入输出T_array);

procedure sort(A : in out T_array);

如果没有with子句,过程sort将无法对A中的元素进行比较排序,因为类型T私有的—它只支持赋值、相等和不等测试,以及一些其他标准属性(例如size)。 ■

Without the with clause, procedure sort would be unable to compare elements of A for ordering, because type T is private—it supports only assignment, testing for equality and inequality, and a few other standard attributes (e.g., size). ■

例 7.51

Example 7.51

Java 中的通用排序例程

Generic sorting routine in Java

Java 和 C# 采用了一种特别干净的约束方法,利用了面向对象类型从父类型或接口继承方法的能力。我们将把对继承的完整讨论推迟到第 10 章。现在,我们注意到它允许 Java 或 C# 程序员要求泛型参数支持一组特定的方法,就像 Haskell 的类型类将可接受参数的类型限制为隐式多态函数一样。在 Java 中,我们可以声明并使用排序例程,如下所示:

Java and C# employ a particularly clean approach to constraints that exploits the ability of object-oriented types to inherit methods from a parent type or interface. We defer a full discussion of inheritance to Chapter 10. For now, we note that it allows the Java or C# programmer to require that a generic parameter support a particular set of methods, much as the type classes of Haskell constrain the types of acceptable parameters to an implicitly polymorphic function. In Java, we might declare and use a sorting routine as follows:

设计与实现

Design & Implementation

7.10 重载和多态

7.10 Overloading and polymorphism

鉴于编译器通常会为通用子程序创建多个代码实例,专门用于给定的一组通用参数,人们可能会想:图 7.2左侧和右侧之间到底有什么区别?答案在于多态代码的通用性。使用重载时,程序员必须为每种类型编写一个单独的 min 例程,虽然编译器会自动从这些例程中进行选择,但它们对其参数执行类似操作的事实纯粹是惯例问题。另一方面,泛型允许编译器为每种所需类型创建适当的版本。调用语法的相似性(以及遵循惯例时生成的代码的相似性)导致一些作者将重载称为临时(特殊情况)多态性。但是,程序员没有特别的理由将多态性视为多个副本:从语义(概念)的角度来看,重载子程序对不止一个事物使用一个名称;多态子程序一个事物。

Given that a compiler will often create multiple instances of the code for a generic subroutine, specialized to a given set of generic parameters, one might be forgiven for wondering: what exactly is the difference between the left and right sides of Figure 7.2? The answer lies in the generality of the polymorphic code. With overloading the programmer must write a separate min routine for every type, and while the compiler will choose among these automatically, the fact that they do something similar with their arguments is purely a matter of convention. Generics, on the other hand, allow the compiler to create an appropriate version for every needed type. The similarity of the calling syntax (and of the generated code, when conventions are followed) has led some authors to refer to overloading as ad hoc (special case) polymorphism. There is no particular reason, however, for the programmer to think of polymorphism in terms of multiple copies: from a semantic (conceptual) point of view, overloaded subroutines use a single name for more than one thing; a polymorphic subroutine is a single thing.

公共静态<T扩展了Comparable<T>> void sort(TA []){

public static <T extends Comparable<T>> void sort(T A[]) {

 

 

 如果 (A[i].compareTo(A[j]) >=0) …

 if (A[i].compareTo(A[j]) >=0) …

 

 

}

}

整数[] myArray = 新整数[50];

Integer[] myArray = new Integer[50];

排序(我的数组);

sort(myArray);

C++ 要求在泛型方法前加上模板<type_args> 前缀,而 Java 则将类型参数放在方法的返回类型前面。extends 子句构成泛型约束:Comparable是 Java 标准库中的一个接口(一组必需的方法);它包括方法compareTo。此方法分别返回 -1、0 或 1,具体取决于当前对象是小于、等于还是大于作为参数传递的对象。编译器会检查以确保传递给sort 的任何数组中的对象都是实现Comparable的类型,因此保证提供compareTo。如果T需要其他接口(即,如果我们想要更多约束),可以使用逗号分隔的列表指定它们:<T extends I1, I2, I3>。■

Where C++ requires a template<type_args> prefix before a generic method, Java puts the type parameters immediately in front of the method's return type. The extends clause constitutes a generic constraint: Comparable is an interface (a set of required methods) from the Java standard library; it includes the method compareTo. This method returns —1, 0, or 1, respectively, depending on whether the current object is less than, equal to, or greater than the object passed as a parameter. The compiler checks to make sure that the objects in any array passed to sort are of a type that implements Comparable, and are therefore guaranteed to provide compareTo. If T had needed additional interfaces (that is, if we had wanted more constraints), they could have been specified with a comma-separated list: <T extends I1, I2, I3>. ■

例 7.52

Example 7.52

C# 中的通用排序例程

Generic sorting routine in C#

C# 语法类似:

C# syntax is similar:

静态 void sort<T>(T[] A) 其中 T : IComparable {

static void sort<T>(T[] A) where T : IComparable {

 

 

 如果 (A[i].CompareTo(A[j]) >= 0) …

 if (A[i].CompareTo(A[j]) >= 0) …

 

 

}

}

int[] myArray = new int[50];

int[] myArray = new int[50];

排序(我的数组);

sort(myArray);

C# 将类型参数放在子例程名称之后,将约束(where子句)放在常规参数列表之后。编译器足够聪明,能够识别出int是原始类型,并生成sort的自定义实现,从而消除了对 Java 的Integer包装器类的需求,并生成了更快的代码。■

C# puts the type parameters after the name of the subroutine, and the constraints (the where clause) after the regular parameter list. The compiler is smart enough to recognize that int is a primitive type, and generates a customized implementation of sort, eliminating the need for Java's Integer wrapper class, and producing faster code. ■

例 7.53

Example 7.53

C++ 中的通用排序例程

Generic sorting routine in C++

一些语言放弃了显式约束,但仍会检查参数的使用方式。例如,在 C++ 中,通用排序例程的标头可以非常简单:

A few languages forgo explicit constraints, but still check how parameters are used. In C++, for example, the header of a generic sorting routine can be extremely simple:

模板<类型名称 T>

template<typename T>

无效排序(TA [],int A_size){…

void sort(T A[], int A_size) { …

没有提到需要比较运算符。泛型的主体可以(尝试)对泛型参数的对象执行任意操作类型,但是如果用不支持该操作的类型来实例化泛型,编译器将宣布静态语义错误。不幸的是,因为泛型的标头不一定指定需要哪些操作,所以程序员很难预测特定实例化是否会导致错误消息。更糟糕的是,在某些情况下,特定实例中提供的类型可能支持泛型代码所需的操作,但该操作可能不会做“正确的事情”。假设在我们的 C++ 排序示例中,sort 代码使用了 < 运算符。对于intdouble,这个运算符将执行我们所期望的操作。但是,对于字符串,它将比较指针,以查看哪个引用字符在内存中具有较低的地址。如果程序员期望比较字典顺序,结果可能会令人惊讶!

No mention is made of the need for a comparison operator. The body of a generic can (attempt to) perform arbitrary operations on objects of a generic parameter type, but if the generic is instantiated with a type that does not support that operation, the compiler will announce a static semantic error. Unfortunately, because the header of the generic does not necessarily specify which operations will be required, it can be difficult for the programmer to predict whether a particular instantiation will cause an error message. Worse, in some cases the type provided in a particular instantiation may support an operation required by the generic's code, but that operation may not do “the right thing.” Suppose in our C++ sorting example that the code for sort makes use of the < operator. For ints and doubles, this operator will do what one would expect. For character strings, however, it will compare pointers, to see which referenced character has a lower address in memory. If the programmer is expecting comparison for lexicographic ordering, the results may be surprising!

为避免意外,最好避免隐式使用泛型参数类型的操作。下一版 C++ 标准可能会纳入显式模板约束的语法 [ SSD13 ]。目前,比较例程可以作为类T的方法、排序例程的额外参数或额外的泛型参数提供。为了实现第一个选项,程序员可以选择模拟 Java 或 C#,将所需的方法封装在类型T可以从中继承的抽象基类中。■

To avoid surprises, it is best to avoid implicit use of the operations of a generic parameter type. The next version of the C++ standard is likely to incorporate syntax for explicit template constraints [SSD13]. For now, the comparison routine can be provided as a method of class T, an extra argument to the sort routine, or an extra generic parameter. To facilitate the first of these options, the programmer may choose to emulate Java or C#, encapsulating the required methods in an abstract base class from which the type T may inherit. ■

隐式实例

Implicit Instantiation

例 7.54

Example 7.54

C++ 中的泛型类实例

Generic class instance in C++

因为类是一种类型,所以通常必须先创建泛型类的实例(即对象),然后才能使用泛型。声明提供了一个提供泛型参数的自然位置:

Because a class is a type, one must generally create an instance of a generic class (i.e., an object) before the generic can be used. The declaration provides a natural place to provide generic arguments:

队列<int,50> *my_queue = 新队列<int,50>(); // C++

queue<int, 50> *my_queue = new queue<int, 50>(); // C++

例 7.55

Example 7.55

Ada 中的通用子程序实例

Generic subroutine instance in Ada

某些语言(其中包括 Ada)还要求通用子程序在使用前必须明确实例化:

Some languages (Ada among them) also require generic subroutines to be instantiated explicitly before they can be used:

过程int_sort是new sort(integer,int_array,“<”);

procedure int_sort is new sort(integer, int_array, “<”);

int_sort(我的数组);

int_sort(my_array);

例 7.56

Example 7.56

C++ 中的隐式实例

Implicit instantiation in C++

其他语言(包括 C++、Java 和 C#)不需要这样做。相反,它们将通用子例程视为一种重载形式。给定示例 7.53中的 C++ 排序例程和以下对象:

Other languages (C++, Java, and C# among them) do not require this. Instead they treat generic subroutines as a form of overloading. Given the C++ sorting routine of Example 7.53 and the following objects:

int int[10]; 复制代码

int ints[10];

双实数[50];

double reals[50];

string strings[30]; // 库类字符串具有字典运算符<

string strings[30]; // library class string has lexicographic operator<

我们可以执行以下操作,而无需明确实例化任何内容:

we can perform the following calls without instantiating anything explicitly:

排序(整数,10);

sort(ints, 10);

排序(实数,50);

sort(reals, 50);

排序(字符串,30);

sort(strings, 30);

在每种情况下,编译器都会隐式实例化适当版本的排序例程。Java 和 C# 有类似的约定。为了保持语言的可管理性,C++ 中隐式实例化的规则比一般解析重载子例程的规则更为严格。特别是,编译器不会强制子例程参数与包含泛型参数的类型表达式匹配(练习 C-7.26)。■

In each case, the compiler will implicitly instantiate an appropriate version of the sort routine. Java and C# have similar conventions. To keep the language manageable, the rules for implicit instantiation in C++ are more restrictive than the rules for resolving overloaded subroutines in general. In particular, the compiler will not coerce a subroutine argument to match a type expression containing a generic parameter (Exercise C-7.26). ■

图 7.4总结了 Ada、C++、Java 和 C# 泛型的特性,以及 Lisp 和 ML 的隐式参数多态性。部分细节的进一步解释见第 C-7.3.2 节

Figure 7.4 summarizes the features of Ada, C++, Java, and C# generics, and of the implicit parametric polymorphism of Lisp and ML. Further explanation of some of the details appears in Section C-7.3.2.

编号07-04-9780124104099
图 7.4 Ada、C++、Java、C#、Lisp 和 ML 中的参数多态性机制。擦除和具体化在第 C-7.3.2 节中讨论。

7.3.2 C++、Java 和 C# 中的泛型

7.3.2 Generics in C++, Java, and C#

通过比较 C++、Java 和 C# 的特性,可以说明泛型设计中的几个关键权衡。C++ 是这三个语言中最具雄心的。它的模板适用于几乎所有需要基本相似但不完全相同的抽象副本的编程任务。Java 和 C# 提供泛型纯粹是为了实现多态性。Java 的设计深受向后兼容需求的影响,不仅与现有版本的语言兼容,而且与现有的虚拟机和库兼容。C# 设计者虽然是在现有语言的基础上构建的,但并没有感到那么受限制。他们从一开始就一直在规划泛型,并能够在 .NET 虚拟机中设计大量新的支持。

Several of the key tradeoffs in the design of generics can be illustrated by comparing the features of C++, Java, and C#. C++ is by far the most ambitious of the three. Its templates are intended for almost any programming task that requires substantially similar but not identical copies of an abstraction. Java and C# provide generics purely for the sake of polymorphism. Java's design was heavily influenced by the desire for backward compatibility, not only with existing versions of the language, but with existing virtual machines and libraries. The C# designers, though building on an existing language, did not feel as constrained. They had been planning for generics from the outset, and were able to engineer substantial new support into the .NET virtual machine.

在07-02-9780124104099 更深入地

IN MORE DEPTH

在配套网站上,我们更详细地讨论了 C++、Java 和 C# 泛型,并考虑了它们的不同设计对错误消息质量、生成代码的速度和大小以及符号的表达能力的影响。我们特别注意到,用于使泛型类和方法支持尽可能广泛的泛型参数类的机制非常不同。

On the companion site we discuss C++, Java, and C# generics in more detail, and consider the impact of their differing designs on the quality of error messages, the speed and size of generated code, and the expressive power of the notation. We note in particular the very different mechanisms used to make generic classes and methods support as broad a class of generic arguments as possible.

7.4 相等性测试和赋值

7.4 Equality Testing and Assignment

对于简单的原始数据类型(例如整数、浮点数或字符),相等性测试和赋值是相对简单的操作,具有明显的语义和明显的实现(逐位比较或复制)。对于更复杂或抽象的数据类型,语义和实现都会出现微妙之处。

For simple, primitive data types such as integers, floating-point numbers, or characters, equality testing and assignment are relatively straightforward operations, with obvious semantics and obvious implementations (bit-wise comparison or copy). For more complicated or abstract data types, both semantic and implementation subtleties arise.

例如,考虑比较两个字符串的问题。表达式s = t是否确定st

Consider for example the problem of comparing two character strings. Should the expression s = t determine whether s and t

 彼此都有别名吗?

 are aliases for one another?

 是否占用整个长度上逐位相同的存储空间?

 occupy storage that is bit-wise identical over its full length?

 包含相同的字符序列?

 contain the same sequence of characters?

 打印出来会一样吗?

 would appear the same if printed?

这些测试中的第二个可能太低级,大多数程序都不会感兴趣;它表明比较可能会因为字符串保留空间中当前未使用的部分存在垃圾而失败。其他三个替代方案在某些情况下可能都很有趣,并且可能会产生不同的结果。

The second of these tests is probably too low level to be of interest in most programs; it suggests the possibility that a comparison might fail because of garbage in currently unused portions of the space reserved for a string. The other three alternatives may all be of interest in certain circumstances, and may generate different results.

在许多情况下,相等的定义归结为左值和右值之间的区别:在存在引用的情况下,表达式是否应仅当它们引用同一个对象时才被视为相等,或者它们引用的对象在某种意义上相等时也被视为相等?第一种选择(引用同一个对象)称为比较。第二种选择(引用相等的对象)称为深度比较。对于复杂的数据结构(例如列表或图形),深度比较可能需要递归遍历。

In many cases the definition of equality boils down to the distinction between l-values and r-values: in the presence of references, should expressions be considered equal only if they refer to the same object, or also if the objects to which they refer are in some sense equal? The first option (refer to the same object) is known as a shallow comparison. The second (refer to equal objects) is called a deep comparison. For complicated data structures (e.g., lists or graphs) a deep comparison may require recursive traversal.

在命令式编程语言中,赋值操作也可能是深的或浅的。在变量的引用模型下,浅赋值a := b将使a引用b所引用的对象。深赋值将创建 b 所引用对象的副本,并使 a 引用该副本。在变量的值模型下,浅赋值将把b的值复制到a中,但如果该值是一个指针(或包含指针的记录),则不会复制指针所引用的对象。

In imperative programming languages, assignment operations may also be deep or shallow. Under a reference model of variables, a shallow assignment a := b will make a refer to the object to which b refers. A deep assignment will create a copy of the object to which b refers, and make a refer to the copy. Under a value model of variables, a shallow assignment will copy the value of b into a, but if that value is a pointer (or a record containing pointers), then the objects to which the pointer(s) refer will not be copied.

例 7.57

Example 7.57

Scheme 中的相等性测试

Equality testing in Scheme

大多数编程语言都采用浅比较和浅赋值。少数语言(尤其是 Python 和 Lisp 和 ML 的各种方言)提供多个比较选项。例如,Scheme 有三个通用的相等性测试函数:

Most programming languages employ both shallow comparisons and shallow assignment. A few (notably Python and the various dialects of Lisp and ML) provide more than one option for comparison. Scheme, for example, has three general-purpose equality-testing functions:

(等式?ab);a 和 b 指的是同一个对象吗?
(等价于 ab);a 和 b 是否在语义上等价?
(相等吗?ab);a 和 b 有同样的递归结构吗?

eq?eqv?都执行浅层比较。前者在某些实现中对于某些类型可能更快;特别是,eqv?需要检测存储在不同位置的相同离散类型的值是否相等;而 eq?则不需要。更简单的eq? 的行为与布尔值、符号(名称)和对(由cons构建的东西)的预期一致,但可以对数字、字符和字符串具有实现定义的行为:

Both eq? and eqv? perform a shallow comparison. The former maybe faster for certain types in certain implementations; in particular, eqv? is required to detect the equality of values of the same discrete type, stored in different locations; eq? is not. The simpler eq? behaves as one would expect for Booleans, symbols (names), and pairs (things built by cons), but can have implementation-defined behavior on numbers, characters, and strings:

(等式?#t #t)⇒ #t(真)
(等式?'foo'foo)⇒ #t
(等式?'(ab)'(ab))⇒ #f (false);由单独的 cons-es 创建
(设((p'(ab)))(等式?pp))⇒ #t;由相同的 cons 创建
(等式?2 2) 取决于具体实现
(等式?“foo” “foo”) 取决于具体实现

在任何特定实现中,数字、字符和字符串测试的工作方式始终相同;如果 ( eq? 2 2 ) 返回 true,则 ( eq? 37 37 ) 也将返回 true。实现可以自由选择哪种行为可以产生最快的代码。

In any particular implementation, numeric, character, and string tests will always work the same way; if (eq? 2 2) returns true, then (eq? 37 37) will return true also. Implementations are free to choose whichever behavior results in the fastest code.

确定 eqv?保证返回truefalse 的具体规则相当复杂。除其他外,它们规定eqv? 的行为应与数字、字符和非空字符串的行为一致,并且如果存在任何情况下两个对象的行为不同,则它们永远不会对eqv?测试为 true 。(但是,相反,对于某些对象(例如函数), eqv ?允许返回false,这些对象在所有情况下的行为都相同。)7 eqv ?谓词比eq? “更不具鉴别力” ,因为当eq?返回true时,eqv?永远不会返回false

The exact rules that govern the situations in which eqv? is guaranteed to return true or false are quite involved. Among other things, they specify that eqv? should behave as one might expect for numbers, characters, and nonempty strings, and that two objects will never test true for eqv? if there are any circumstances under which they would behave differently. (Conversely, however, eqv? is allowed to return false for certain objects—functions, for example—that would behave identically in all circumstances.)7 The eqv? predicate is “less discriminating” than eq?, in the sense that eqv? will never return false when eq? returns true.

对于结构(列表),如果eqv?的参数引用不同的根cons单元,则返回false。在许多程序中,这并不是理想的行为。equal ?谓词递归遍历两个列表,以查看它们的内部结构是否相同以及它们的叶子是否eqv?。如果程序员使用 Scheme 的命令式功能创建循环列表,则equal?谓词可能会导致无限循环。 ■

For structures (lists), eqv? returns false if its arguments refer to different root cons cells. In many programs this is not the desired behavior. The equal? predicate recursively traverses two lists to see if their internal structure is the same and their leaves are eqv?. The equal? predicate may lead to an infinite loop if the programmer has used the imperative features of Scheme to create a circular list. ■

深度赋值相对较少见。它们主要用于分布式计算,特别是用于远程过程调用 (RPC) 系统中的参数传递。这些将在 C-13.5.4 节中讨论。

Deep assignments are relatively rare. They are used primarily in distributed computing, and in particular for parameter passing in remote procedure call (RPC) systems. These will be discussed in Section C-13.5.4.

对于用户定义的抽象,没有任何一种语言指定的相等性测试或赋值机制可能在所有情况下都产生所需的结果。具有复杂数据抽象机制的语言通常允许程序员为每种新数据类型定义比较和赋值运算符 - 或者指定不允许相等性测试和/或赋值。

For user-defined abstractions, no single language-specified mechanism for equality testing or assignment is likely to produce the desired results in all cases. Languages with sophisticated data abstraction mechanisms usually allow the programmer to define the comparison and assignment operators for each new data type—or to specify that equality testing and/or assignment is not allowed.

在07-01-9780124104099检查你的理解

Check Your Understanding

26. 解释隐式和显式参数多态性的区别。它们的比较优势是什么?

26. Explain the distinction between implicit and explicit parametric polymorphism. What are their comparative advantages?

27. 什么是鸭子类型?它和多态性有什么关系?它出现在哪些语言中?

27. What is duck typing? What is its connection to polymorphism? In what languages does it appear?

28. 解释重载和泛型的区别。为什么前者有时被称为特设多态性?

28. Explain the distinction between overloading and generics. Why is the former sometimes called ad hoc polymorphism?

29. 泛型的主要用途是什么?在何种意义上,泛型在 C++ 和 Ada 中的用途比在 Java 和 C# 中的用途更广泛?

29. What is the principal purpose of generics? In what sense do generics serve a broader purpose in C++ and Ada than they do in Java and C#?

30. 在什么情况下语言实现可以在泛型的不同实例之间共享代码?

30. Under what circumstances can a language implementation share code among separate instances of a generic?

31. 什么是容器类?它们和泛型有什么关系?

31. What are container classes? What do they have to do with generics?

32. 通用参数受约束是什么意思?解释显式约束和隐式约束之间的区别。描述如何使用接口类来指定 Java 和 C# 中的约束。

32. What does it mean for a generic parameter to be constrained? Explain the difference between explicit and implicit constraints. Describe how interface classes can be used to specify constraints in Java and C#.

33. 为什么 C# 接受 int 作为泛型参数,而 Java 却不接受?

33. Why will C# accept int as a generic argument, but Java won't?

34. 在什么情况下C++会隐式实例化泛型函数?

34. Under what circumstances will C++ instantiate a generic function implicitly?

35. 为什么平等测试比乍一看更加微妙?

35. Why is equality testing more subtle than it first appears?

7.5 总结和结束语

7.5 Summary and Concluding Remarks

本章概述了类型的基本概念。在典型的编程语言中,类型有两个主要用途:它们为许多操作提供隐式上下文,使程序员无需显式指定上下文,并且允许编译器捕获各种常见的编程错误。在讨论类型时,我们注意到区分指称、结构和基于抽象的观点有时会有所帮助,这些观点分别从类型的值、子结构和支持的操作的角度来看待类型。

This chapter has surveyed the fundamental concept of types. In the typical programming language, types serve two principal purposes: they provide implicit context for many operations, freeing the programmer from the need to specify that context explicitly, and they allow the compiler to catch a wide variety of common programming errors. When discussing types, we noted that it is sometimes helpful to distinguish among denotational, structural, and abstraction-based points of view, which regard types, respectively, in terms of their values, their substructure, and the operations they support.

在典型的编程语言中,类型系统由一组内置类型、定义新类型的机制以及类型等价类型兼容性类型推断的规则组成。类型等价确定两个值或命名对象何时具有相同类型。类型兼容性确定何时可以在“期望”另一种类型的上下文中使用一种类型的值。类型推断根据表达式组件的类型或(有时)周围上下文确定表达式的类型。如果一种语言从不允许将操作应用于不支持该操作的对象,则称该语言为强类型;如果一种语言在编译时强制执行强类型,则称该语言为静态类型。

In a typical programming language, the type system consists of a set of built-in types, a mechanism to define new types, and rules for type equivalence, type compatibility, and type inference. Type equivalence determines when two values or named objects have the same type. Type compatibility determines when a value of one type maybe used in a context that “expects” another type. Type inference determines the type of an expression based on the types of its components or (sometimes) the surrounding context. A language is said to be strongly typed if it never allows an operation to be applied to an object that does not support it; a language is said to be statically typed if it enforces strong typing at compile time.

我们介绍了常用内置类型、枚举、子范围和常用类型构造函数的术语(后者将在第 8 章中详细介绍)。我们讨论了类型等价、兼容性和推断的几种不同方法。我们还研究了类型转换强制非转换强制类型转换。在类型等价方面,我们对比了结构和基于名称的方法,并注意到虽然名称等价似乎越来越受欢迎,但结构等价仍然受到拥护。

We introduced terminology for the common built-in types and for enumerations, subranges, and the common type constructors (more on the latter will appear in Chapter 8). We discussed several different approaches to type equivalence, compatibility, and inference. We also examined type conversion, coercion, and nonconverting casts. In the area of type equivalence, we contrasted the structural and name-based approaches, noting that while name equivalence appears to have gained in popularity, structural equivalence retains its advocates.

我们扩展了3.5.2 节中介绍的内容,探索了几种多态性样式,所有这些样式都允许子程序(或类的方法)操作多种类型的值,只要它们仅以它们的类型支持的方式使用这些值。我们特别关注了参数多态性,其中代码将操作的值的类型作为额外参数传递给它,无论是隐式还是显式。隐式替代方案出现在 ML 及其后代的静态类型中,以及 Lisp、Smalltalk 和许多其他语言的动态类型中。显式替代方案出现在许多现代语言的泛型中。在第 10 章中,我们将考虑与子类型多态性相关的主题。

Expanding on material introduced in Section 3.5.2, we explored several styles of polymorphism, all of which allow a subroutine—or the methods of a class—to operate on values of multiple types, so long as they only use those values in ways their types support. We focused in particular on parametric polymorphism, in which the types of the values on which the code will operate are passed to it as extra parameters, implicitly or explicitly. The implicit alternative appears in the static typing of ML and its descendants, and in the dynamic typing of Lisp, Smalltalk, and many other languages. The explicit alternative appears in the generics of many modern languages. In Chapter 10 we will consider the related topic of subtype polymorphism.

在讨论隐式参数多态性时,我们投入了大量精力在 ML 中的类型检查上,其中编译器使用复杂的推理系统在编译时确定类型错误(尝试对不支持的类型执行操作)是否可能发生在运行时 — 所有这些都无需访问源代码中的类型声明。在讨论泛型时,我们探索了表达泛型参数约束的其他方法。我们还考虑了实现策略。作为示例,我们在配套网站上对比了 C++、Java 和 C# 的泛型功能。

In our discussion of implicit parametric polymorphism, we devoted considerable attention to type checking in ML, where the compiler uses a sophisticated system of inference to determine, at compile time, whether a type error (an attempt to perform an operation on a type that doesn't support it) could ever occur at run time—all without access to type declarations in the source code. In our discussion of generics we explored alternative ways to express constraints on generic parameters. We also considered implementation strategies. As examples, we contrasted (on the companion site) the generic facilities of C++, Java, and C#.

与前面的章节相比,我们对类型的研究或许更能凸显出不同语言设计者在理念上的根本差异。正如我们所见,一些语言使用变量来命名值,而另一些语言则使用引用。一些语言在编译时进行全部或大部分类型检查,而另一些语言则等到运行时才进行。在那些在编译时进行检查的语言中,一些使用名称等价,而另一些使用结构等价。一些语言避免类型强制,而另一些语言则接受它们。一些语言避免重载,而另一些语言又接受它们。在每种情况下,设计方案的选择都反映了相互竞争的语言目标之间非平凡的权衡,包括表现力、编程的简易性、错误发现的质量和时间、调试和维护的简易性、编译成本以及运行时性能。

More so, perhaps, than in previous chapters, our study of types has highlighted fundamental differences in philosophy among language designers. As we have seen, some languages use variables to name values; others, references. Some languages do all or most of their type checking at compile time; others wait until run time. Among those that check at compile time, some use name equivalence; others, structural equivalence. Some languages avoid type coercions; others embrace them. Some avoid overloading; others again embrace them. In each case, the choice among design alternatives reflects nontrivial tradeoffs among competing language goals, including expressiveness, ease of programming, quality and timing of error discovery, ease of debugging and maintenance, compilation cost, and run-time performance.

7.6 练习

7.6 Exercises

7.1 自 20 世纪 70 年代以来开发的大多数静态类型语言(包括 Java、C# 和 Pascal 的后代)都使用某种形式的类型名称等价性。结构等价性是不是一个坏主意?为什么?

7.1 Most statically typed languages developed since the 1970s (including Java, C#, and the descendants of Pascal) use some form of name equivalence for types. Is structural equivalence a bad idea? Why or why not?

7.2 在以下代码中,编译器会认为哪些变量在结构等价下具有兼容类型?在严格名称等价下?在宽松名称等价下?类型 T = 整数数组 [1..10] S =T A : T B : T C : S D : 整数数组 [1..10]

 

  

 

 

 

 

7.2 In the following code, which of the variables will a compiler consider to have compatible types under structural equivalence? Under strict name equivalence? Under loose name equivalence?

 type T = array [1..10] of integer

  S =T

 A : T

 B : T

 C : S

 D : array [1..10] of integer

7.3 考虑以下声明:1. type cell --前向声明2. type cell_ptr = 指向 cell 的指针3. x : cell 4. type cell = record 5. val : integer 6. next : cell_ptr 7. y : cell第 4 行的声明是否应该被说成是引入了别名类型?在严格名称等价性下,xy是否应该具有相同的类型?解释一下。

 

 

 

 

 

 

 

7.3 Consider the following declarations:

 1. type cell --a forward declaration

 2. type cell_ptr = pointer to cell

 3. x : cell

 4. type cell = record

 5.  val : integer

 6.  next : cell_ptr

 7. y : cell

Should the declaration at line 4 be said to introduce an alias type? Under strict name equivalence, should x and y have the same type? Explain.

7.4 假设您正在实现一个 Ada 编译器,并且必须支持对具有程序员指定小数位数的 32 位定点二进制数进行算术运算。描述您需要生成的对两个定点数进行加、减、乘或除的代码。您应该假设硬件只提供整数和 IEEE 浮点的算术指令。您可以假设整数指令保留全精度;特别是,整数乘法产生 64 位结果。您的描述应该足够通用,以处理具有不同小数位数的操作数和结果。

7.4 Suppose you are implementing an Ada compiler, and must support arithmetic on 32-bit fixed-point binary numbers with a programmer-specified number of fractional bits. Describe the code you would need to generate to add, subtract, multiply, or divide two fixed-point numbers. You should assume that the hardware provides arithmetic instructions only for integers and IEEE floating-point. You may assume that the integer instructions preserve full precision; in particular, integer multiplication produces a 64-bit result. Your description should be general enough to deal with operands and results that have different numbers of fractional bits.

7.5  20 世纪 80 年代初,当 Sun Microsystems 将 Berkeley Unix 从 Digital VAX 移植到 Motorola 680x0 时,许多 C 语言程序停止运行,必须进行修复。实际上,680x0 暴露了某些类型的程序错误,这些错误在 VAX 上是可以“侥幸逃脱”的。其中一类错误出现在使用多种整数大小(例如短整数和长整数)的程序中,这是因为 VAX 是小端机,而 680x0 是大端机(第 C-5.2 节)。另一类错误出现在同时操作空字符串和空字符串的程序中。它出现在因为 VAX 上 Unix 进程地址空间中的零位置始终包含零,而 680x0 上的相同位置不在该地址空间中,如果使用则会产生保护错误。对于这两类错误,请给出可以在 VAX 上运行但在 680x0 上无法运行的程序片段的示例。

7.5 When Sun Microsystems ported Berkeley Unix from the Digital VAX to the Motorola 680x0 in the early 1980s, many C programs stopped working, and had to be repaired. In effect, the 680x0 revealed certain classes of program bugs that one could “get away with” on the VAX. One of these classes of bugs occurred in programs that use more than one size of integer (e.g., short and long), and arose from the fact that the VAX is a little-endian machine, while the 680x0 is big-endian (Section C-5.2). Another class of bugs occurred in programs that manipulate both null and empty strings. It arose from the fact that location zero in a Unix process's address space on the VAX always contained a zero, while the same location on the 680x0 is not in the address space, and will generate a protection error if used. For both of these classes of bugs, give examples of program fragments that would work on a VAX but not on a 680x0.

7.6  Ada 为整数类型提供了两个“余数”运算符,remmod,定义如下 [ Ame83,第 4.5.5 节]:

7.6 Ada provides two “remainder” operators, rem and mod for integer types, defined as follows [Ame83, Sec. 4.5.5]:

整数除法和余数由关系A = (A/B)*B + (A rem B)定义,其中(A rem B)具有 A 的符号且绝对值小于 B 的绝对值。整数除法满足恒等式(-A)/B = -(A/B) = A/(-B)

Integer division and remainder are defined by the relation A = (A/B)*B + (A rem B), where (A rem B) has the sign of A and an absolute value less than the absolute value of B. Integer division satisfies the identity (-A)/B = -(A/B) = A/(-B).

模数运算的结果是(A mod B)具有B的符号和小于B的绝对值的绝对值;此外,对于某个整数值N,该结果必须满足关系A = B*N + (A mod B)

The result of the modulus operation is such that (A mod B) has the sign of B and an absolute value less than the absolute value of B; in addition, for some integer value N, this result must satisfy the relation A = B*N + (A mod B).

给出AB的值,其中A rem BA mod B不同。出于什么目的,一个操作会比另一个更有用?同时提供两个操作是否有意义,还是有点过头了?

再考虑一下 C 的 % 运算符和 Pascal 的 mod 运算符。这些语言的设计者可以选择类似于 Ada 的rem或其mod的语义。他们选择了哪一个?你认为他们的选择正确吗?

Give values of A and B for which A rem B and A mod B differ. For what purposes would one operation be more useful than the other? Does it make sense to provide both, or is it overkill?

Consider also the % operator of C and the mod operator of Pascal. The designers of these languages could have picked semantics resembling those of either Ada's rem or its mod. Which did they pick? Do you think they made the right choice?

7.7 考虑在 Pascal 中对集合表达式执行范围检查的问题。假设一个集合可能包含许多元素,其中一些元素在编译时可能是已知的,请描述编译器可能维护的信息,以便跟踪已知属于该集合的元素和未知元素的可能范围。然后解释如何为以下集合运算更新此信息:并集、交集和差集。目标是确定 (1) 何时可以在运行时消除子范围检查以及 (2) 何时可以在编译时报告子范围错误。请记住,编译器不可能做得完美:不可避免地会执行一些不必要的运行时检查,并且一些必然会导致错误的操作将不会在编译时被捕获。目标是以合理的成本尽可能好地完成工作。

7.7 Consider the problem of performing range checks on set expressions in Pascal. Given that a set may contain many elements, some of which may be known at compile time, describe the information that a compiler might maintain in order to track both the elements known to belong to the set and the possible range of unknown elements. Then explain how to update this information for the following set operations: union, intersection, and difference. The goal is to determine (1) when subrange checks can be eliminated at run time and (2) when subrange errors can be reported at compile time. Bear in mind that the compiler cannot do a perfect job: some unnecessary run-time checks will inevitably be performed, and some operations that must always result in errors will not be caught at compile time. The goal is to do as good a job as possible at reasonable cost.

7.8 7.2.2 节中,我们引入了通用引用类型(C 语言中的void *)的概念,该类型引用未知类型的对象。使用此类引用,按照7.3.1 节中的建议,在 C 语言中实现“穷人的通用队列” 。哪些地方需要类型转换?为什么?给出一个由于缺乏类型检查而在运行时导致灾难性失败的队列使用示例。

7.8 In Section 7.2.2 we introduced the notion of a universal reference type (void * in C) that refers to an object of unknown type. Using such references, implement a “poor man's generic queue” in C, as suggested in Section 7.3.1. Where do you need type casts? Why? Give an example of a use of the queue that will fail catastrophically at run time, due to the lack of type checking.

7.9 用 Ada、Java 或 C#重写图 7.3的代码。

7.9 Rewrite the code of Figure 7.3 in Ada, Java, or C#.

7.10 

7.10 

(a)给出 练习 6.19 的通用解决方案。

(a) Give a generic solution to Exercise 6.19.

(b) 将该解决方案翻译成 Ada、Java 或 C#。

(b) Translate this solution into Ada, Java, or C#.

7.11 使用你最喜欢的具有泛型的语言,编写以下抽象的简单版本的代码:

7.11 In your favorite language with generics, write code for simple versions of the following abstractions:

(一个) 堆栈,以链表形式实现

(a) a stack, implemented as a linked list

(b) 优先级队列,以跳跃表或嵌入数组的偏序树的形式实现。

(b) a priority queue, implemented as a skip list or a partially ordered tree embedded in an array

(c) 以哈希表形式实现的字典(映射)

(c) a dictionary (mapping), implemented as a hash table

7.12 图 7.3将整数max_items作为通用参数传递给队列抽象。编写代码的替代版本,将max_items作为队列构造函数的参数。通用参数版本的优点是什么?

7.12 Figure 7.3 passes integer max_items to the queue abstraction as a generic parameter. Write an alternative version of the code that makes max_items a parameter to the queue constructor instead. What is the advantage of the generic parameter version?

7.13使用 OCaml 或 SML 函子重写 示例 7.507.52的通用排序例程(带有约束)。

7.13 Rewrite the generic sorting routine of Examples 7.507.52 (with constraints) using OCaml or SML functors.

7.14 充实示例 7.53中的 C++ 排序例程。说明当要求对char*字符串数组进行排序时,此例程会“做错事” 。

7.14 Flesh out the C++ sorting routine of Example 7.53. Demonstrate that this routine does “the wrong thing” when asked to sort an array of char* strings.

7.15 示例 7.53中,我们提到了在 C++ 中定义通用排序例程时使比较需求更加明确的三种方法:将比较例程设为通用参数类T的方法、排序例程的额外参数或额外的通用参数。实现这些选项并讨论它们的优缺点。

7.15 In Example 7.53 we mentioned three ways to make the need for comparisons more explicit when defining a generic sort routine in C++: make the comparison routine a method of the generic parameter class T, an extra argument to the sort routine, or an extra generic parameter. Implement these options and discuss their comparative strengths and weaknesses.

7.16 上一个练习中的问题的另一个解决方案是将排序例程设为排序器类的一个方法。然后可以将比较例程作为构造函数参数传递到该类中。实现此选项并将其与上一个练习中的选项进行比较。

7.16 Yet another solution to the problem of the previous exercise is to make the sorting routine a method of a sorter class. The comparison routine can then be passed into the class as a constructor argument. Implement this option and compare it to those of the previous exercise.

7.17 考虑以下 C++ 代码框架:#include <list> using std::list; class foo { … class bar : public foo { … static void print_all(list<foo*>​​ &L) { … list<foo*>​​ LF; list<bar*> LB; print_all(LF); // 工作正常print_all(LB); // 静态语义错误解释为什么编译器不允许第二次调用。举一个例子说明如果允许,可能发生的坏事。

 

 

 

 

 

 

 

 

 

 

7.17 Consider the following code skeleton in C++:

 #include <list>

 using std::list;

 class foo { …

 class bar : public foo { …

 static void print_all(list<foo*> &L) { …

 list<foo*> LF;

 list<bar*> LB;

 

 print_all(LF); // works fine

 print_all(LB); // static semantic error

Explain why the compiler won't allow the second call. Give an example of bad things that could happen if it did.

7.18  C++ 的最初设计者 Bjarne Stroustrup 曾将模板描述为“一种遵循 C++ 的作用域、命名和类型规则的巧妙的宏” [ Str13,第二版,第 257 页]。两者的相似性有多高?模板能做什么而宏不能做什么?宏能做什么而模板不能做什么?

7.18 Bjarne Stroustrup, the original designer of C++, once described templates as “a clever kind of macro that obeys the scope, naming, and type rules of C++” [Str13, 2nd ed., p. 257]. How close is the similarity? What can templates do that macros can't? What do macros do that templates don't?

7.19 9.3.1 节中,我们注意到 Ada 83 不允许将子程序作为参数传递,但是使用泛型可以实现一些相同的效果。假设我们想要将一个函数应用于数组的每个成员。我们可以在 Ada 83 中编写以下内容:泛型类型 item 是 private;类型 item_array 是 item 的数组 (integer range <>);使用函数 F(it : in item) return item; procedure apply_to_array(A : in out item_array); procedure apply_to_array(A : in out item_array) is begin for i in A'first..A'last loop A(i) := F(A(i)); end loop; end apply_to_array;给定一个整数数组scores和一个整数函数foo,我们可以编写:procedure apply_to_ints is new apply_to_array(integer, int_array, foo); apply_to_ints(scores);这种机制有多通用?它的局限性是什么?它是否是正式(即第二类,而不是第三类)子程序的合理替代品?

 

  

  

  

 

 

 

  

   

  

 



 

  

 

 

7.19 In Section 9.3.1 we noted that Ada 83 does not permit subroutines to be passed as parameters, but that some of the same effect can be achieved with generics. Suppose we want to apply a function to every member of an array. We might write the following in Ada 83:

 generic

  type item is private;

  type item_array is array (integer range <>) of item;

  with function F(it : in item) return item;

 procedure apply_to_array(A : in out item_array);

 procedure apply_to_array(A : in out item_array) is

 begin

  for i in A'first..A'last loop

   A(i) := F(A(i));

  end loop;

 end apply_to_array;

Given an array of integers, scores, and a function on integers, foo, we can write:

 procedure apply_to_ints is

  new apply_to_array(integer, int_array, foo);

 

 apply_to_ints(scores);

How general is this mechanism? What are its limitations? Is it a reasonable substitute for formal (i.e., second-class, as opposed to third-class) subroutines?

7.20修改 图 7.3的代码或练习 7.12的解决方案,使其在尝试将项目放入已满队列中或从空队列中出队时引发异常。

7.20 Modify the code of Figure 7.3 or your solution to Exercise 7.12 to throw an exception if an attempt is made to enqueue an item in a full queue, or to dequeue an item from an empty queue.

在07-02-9780124104099 7.21–7.27 更深入。

7.21–7.27  In More Depth.

7.7 探索

7.7 Explorations

7.28 一些语言定义指定了数据类型在内存中的特定表示,而另一些语言定义仅指定了这些类型的语义行为。对于后一类语言,一些实现保证了特定的表示,而另一些实现保留了在不同情况下选择不同表示的权利。您更喜欢哪种方法?为什么?

7.28 Some language definitions specify a particular representation for data types in memory, while others specify only the semantic behavior of those types. For languages in the latter class, some implementations guarantee a particular representation, while others reserve the right to choose different representations in different circumstances. Which approach do you prefer? Why?

7.29 研究Strom 等人在 Hermes 编程语言中采用的类型状态机制 [ SBG + 91 ]。讨论其与 Java 和 C# 中的明确赋值概念的关系(第 6.1.3 节)。

7.29 Investigate the typestate mechanism employed by Strom et al. in the Hermes programming language [SBG+91]. Discuss its relationship to the notion of definite assignment in Java and C# (Section 6.1.3).

7.30 最近有几个项目试图通过在脚本语言中添加可选的类型声明来模糊静态和动态类型之间的界限。这些声明支持渐进式类型化策略,即程序员最初以传统的脚本风格编写,然后逐步添加声明以提高可靠性或降低运行时成本。了解分别由 Google、Facebook 和 Microsoft 推广的 Dart、Hack 和 TypeScript 语言。你对此有何印象?你认为在实践中将声明改造成最初没有声明的程序有多容易?

7.30 Several recent projects attempt to blur the line between static and dynamic typing by adding optional type declarations to scripting languages. These declarations support a strategy of gradual typing, in which programmers initially write in a traditional scripting style and then add declarations incrementally to increase reliability or decrease run-time cost. Learn about the Dart, Hack, and TypeScript languages, promoted by Google, Facebook, and Microsoft, respectively. What are your impressions? How easy do you think it will be in practice to retrofit declarations into programs originally developed without them?

7.31 研究标准 ML、OCaml、Haskell 和 F# 的类型系统。它们的主要区别是什么?语言设计者做出的不同选择可能由什么原因造成?

7.31 Research the type systems of Standard ML, OCaml, Haskell, and F#. What are the principal differences? What might explain the different choices made by the language designers?

7.32 用 C++ 或 Ada 编写一个程序,从同一模板/通用类型创建至少两个具体类型或子例程。将代码编译为汇编语言并查看结果。描述从源代码到目标代码的映射。

7.32 Write a program in C++ or Ada that creates at least two concrete types or subroutines from the same template/generic. Compile your code to assembly language and look at the result. Describe the mapping from source to target code.

7.33 虽然 Haskell 不包含泛型(其参数多态性是隐式的),但其类型类可以被视为类型约束的泛化。了解有关类型类的更多信息。讨论它们与多态函数的相关性以及更一般的用途。您可能希望提前阅读第 11.5.2 节中有关monad的讨论。

7.33 While Haskell does not include generics (its parametric polymorphism is implicit), its type classes can be considered a generalization of type constraints. Learn more about type classes. Discuss their relevance to polymorphic functions, as well as more general uses. You might want to look ahead to the discussion of monads in Section 11.5.2.

7.34研究 Black 等人在 Emerald 编程语言 [ BHJL07 ] 中采用的类型一致性概念。讨论一致性与 ML 的类型推断以及面向对象语言的基于类的类型化之间的关系。

7.34 Investigate the notion of type conformance, employed by Black et al. in the Emerald programming language [BHJL07]. Discuss how conformance relates to the type inference of ML and to the class-based typing of object-oriented languages.

7.35  C++11 引入了所谓的可变参数模板,它采用可变数量的泛型参数。阅读它们的工作原理。说明如何使用它们将格式化输出的常用语法cout << expr 1 << ... << expr n替换为print ( expr 1 , ..., expr n ),同时保留完整的静态类型检查。

7.35 C++11 introduces so-called variadic templates, which take a variable number of generic parameters. Read up on how these work. Show how they might be used to replace the usual cout << expr1 << … << exprn syntax of formatted output with print(expr1, …, exprn), while retaining full static type checking.

在07-02-9780124104099 7.36–7.38 更深入。

7.36–7.38  In More Depth.

7.8 书目注释

7.8 Bibliographic Notes

有关本章中提到的各种编程语言的一般信息,请参见附录 A以及第 1 章和6 章的参考书目注释。Welsh、Sneeringer 和 Hoare [ WSH77 ] 对原始 Pascal 定义进行了批评,特别强调了其类型系统。Tanenbaum 对 Pascal 和 Algol 68 的比较也主要侧重于类型 [ Tan78 ]。Cleaveland [ Cle86 ] 对本章中的许多问题进行了一本书长度的研究。Pierce [ Pie02 ] 对这个主题进行了正式而详细的现代报道。ACM 编程语言特别兴趣小组于 2003 年启动了每两年一次的语言设计和实现类型研讨会。

References to general information on the various programming languages mentioned in this chapter can be found in Appendix A, and in the Bibliographic Notes for Chapters 1 and 6. Welsh, Sneeringer, and Hoare [WSH77] provide a critique of the original Pascal definition, with a particular emphasis on its type system. Tanenbaum's comparison of Pascal and Algol 68 also focuses largely on types [Tan78]. Cleaveland [Cle86] provides a book-length study of many of the issues in this chapter. Pierce [Pie02] provides a formal and detailed modern coverage of the subject. The ACM Special Interest Group on Programming Languages launched a biennial workshop on Types in Language Design and Implementation in 2003.

我们所说的类型的外延模型源自 Hoare [ DDH72 ]。第 4 章的参考书目注释中讨论了编程语言整体语义的外延公式。一项相关但不同的工作使用代数技术来形式化数据抽象;主要参考文献包括 Guttag [ Gut77 ] 和 Goguen 等人 [ GTW78 ]。Milner 的原始论文 [ Mil78 ] 是关于 ML 中类型推断的开创性参考文献。Mairson [ Mai90 ] 证明统一 ML 类型的成本为O (2n ),其中n是程序的长度。幸运的是,成本与程序的类型表达式的大小成线性关系,因此最坏的情况只会出现在语义过于复杂以至于人类无法理解的程序中。

What we have referred to as the denotational model of types originates with Hoare [DDH72]. Denotational formulations of the overall semantics of programming languages are discussed in the Bibliographic Notes for Chapter 4. A related but distinct body of work uses algebraic techniques to formalize data abstraction; key references include Guttag [Gut77] and Goguen et al. [GTW78]. Milner's original paper [Mil78] is the seminal reference on type inference in ML. Mairson [Mai90] proves that the cost of unifying ML types is O(2n),where n is the length of the program. Fortunately, the cost is linear in the size of the program's type expressions, so the worst case arises only in programs whose semantics are too complex for a human being to understand anyway.

Hoare [ Hoa75 ] 讨论了变量引用模型下递归类型的定义。Cardelli 和 Wegner 调查了与多态性、重载和抽象相关的问题 [ CW85 ]。万维网的字符模型标准对多语言字符集的微妙之处和复杂性进行了极具可读性的介绍 [ Wor05 ]。

Hoare [Hoa75] discusses the definition of recursive types under a reference model of variables. Cardelli and Wegner survey issues related to polymorphism, overloading, and abstraction [CW85]. The Character Model standard for the World Wide Web provides a remarkably readable introduction to the subtleties and complexities of multilingual character sets [Wor05].

Garcia 等人对 ML、C++、Haskell、Eiffel、Java 和 C# [ GJL + 03 ] 中的泛型功能进行了详细比较。Kennedy 和 Syme [ KS01 ]描述了 C# 的泛型功能。Java 泛型基于 Bracha 等人的工作 [ BOSW98 ]。Erwin Unruh 因发现 C++ 模板可以诱使编译器执行非平凡计算而受到赞誉。他的具体示例 ( www.erwin-unruh.de/primorig.html ) 并未编译,但会导致编译器生成一系列包含前n 个数的错误消息。Abrahams 和 Gurtovoy 提供了一本书长度的模板元编程[ AG05 ] 的论述,该领域源于这一发现。

Garcia et al. provide a detailed comparison of generic facilities in ML, C++, Haskell, Eiffel, Java, and C# [GJL+03]. The C# generic facility is described by Kennedy and Syme [KS01]. Java generics are based on the work of Bracha et al. [BOSW98]. Erwin Unruh is credited with discovering that C++ templates could trick the compiler into performing nontrivial computation. His specific example (www.erwin-unruh.de/primorig.html) did not compile, but caused the compiler to generate a sequence of n error messages, embedding the first n primes. Abrahams and Gurtovoy provide a book-length treatment of template metaprogramming [AG05], the field that grew out of this discovery.


1回想一下,除非另有说明,否则我们非正式地使用术语“对象”来指代任何可能有名称的事物。我们将在第 10 章中学习的面向对象语言为该术语赋予了更狭义、更正式的含义。

1 Recall that unless otherwise noted we are using the term “object” informally to refer to anything that might have a name. Object-oriented languages, which we will study in Chapter 10, assign a narrower, more formal, meaning to the term.

2更具体地说,C 要求这些类型的范围分别对应至少 1、2、2、4 和 8 个字节的长度。实际上,可以发现一些实现中普通 int 的长度为 2、4 或 8 个字节,其中一些实现与shorts大小相同但比longs短,还有一些实现与longs大小相同但比shorts长。

2 More specifically, C requires ranges for these types corresponding to lengths of at least 1, 2, 2, 4, and 8 bytes, respectively. In practice, one finds implementations in which plain int s are 2, 4, or 8 bytes long, including some in which they are the same size as shorts but shorter than longs, and some in which they are the same size as longs, and longer than shorts.

3讽刺的是,它对结构使用了名称等价性。

3 Ironically, it uses name equivalence for structs.

4罗宾·米尔纳(Robin Milner,1934-2010),剑桥大学计算机实验室教授,不仅负责机器学习及其类型系统的开发,还负责可计算函数逻辑(为机器辅助证明构造提供了形式基础)和通信系统演算(提供了并发性的一般理论)。他于 1991 年获得 ACM 图灵奖。

4 Robin Milner (1934–2010), of Cambridge University's Computer Laboratory, was responsible not only for the development of ML and its type system, but for the Logic of Computable Functions, which provides a formal basis for machine-assisted proof construction, and the Calculus of Communicating Systems, which provides a general theory of concurrency. He received the ACM Turing Award in 1991.

5由于 OCaml 中的函数是自动柯里化的,因此多个参数实际上比这里建议的要复杂一些;有关详细信息,请参阅第 11.6 节。

5 Multiple arguments are actually somewhatmore complicated than suggested here, due to the fact that functions in OCaml are automatically curried; see Section 11.6 for more details.

6 “鸭子测试”这一俗语的起源尚不确定,但至少可以追溯到 20 世纪初。除其他外,该测试在 20 世纪 40 年代和 50 年代被广泛引用,作为识别所谓共产主义同情者的一种手段。

6 The origins of this “duck test” colloquialism are uncertain, but they go back at least as far as the early 20th century. Among other things, the test was widely cited in the 1940s and 50s as a means of identifying supposed Communist sympathizers.

7值得注意的是,当比较不同类型的数值时, eqv?也可以返回 false: (eqv? 1 1.0)可能计算为#f。对于数字代码,通常需要单独的 = 函数: (= val1 val2 ) 将执行必要的强制并测试数字相等性(受舍入误差影响)。

7 Significantly, eqv? is also allowed to return false when comparing numeric values of different types: (eqv? 1 1.0) may evaluate to #f. For numeric code, one generally wants the separate = function: (= val1 val2) will perform the necessary coercion and test for numeric equality (subject to rounding errors).

8

复合类型

Composite Types

第 7 章 介绍了类型的概念作为一种组织计算机程序操纵的许多值和对象的方式。它还引入了内置类型和复合类型的术语。正如我们在7.1.4 节中提到的,复合类型是通过使用类型构造函数将一个或多个更简单的类型连接在一起而形成的从指称的角度来看,构造函数可以建模为集合上的操作,每个集合代表一个更简单的类型。

Chapter 7 introduced the notion of types as a way to organize the many values and objects manipulated by computer programs. It also introduced terminology for both built-in and composite types. As we noted in Section 7.1.4, composite types are formed by joining together one or more simpler types using a type constructor. From a denotational perspective, the constructors can be modeled as operations on sets, with each set representing one of the simpler types.

在本章中,我们将概述最重要的类型构造函数:记录、数组、字符串、集合、指针、列表和文件。在记录部分,我们还将考虑变体(联合)和元组。在指针部分,我们将更详细地介绍第6.1.2 节中介绍的变量的值和引用模型,以及第 3.2 节中介绍的堆管理问题。文件部分(主要在配套网站上)将讨论输入和输出机制。

In the current chapter we will survey the most important type constructors: records, arrays, strings, sets, pointers, lists, and files. In the section on records we will also consider both variants (unions) and tuples. In the section on pointers, we will take a more detailed look at the value and reference models of variables introduced in Section 6.1.2, and the heap management issues introduced in Section 3.2. The section on files (mostly on the companion site) will include a discussion of input and output mechanisms.

8.1 记录(结构)

8.1 Records (Structures)

记录类型允许将异构类型的相关数据一起存储和操作。记录最初由 Cobol 引入,也出现在 Algol 68 中,后者将其称为结构体,并引入了关键字struct。许多现代语言,包括 C 及其后代,都使用了 Algol 术语。Fortran 90 简单地将其记录称为“类型”:它们是除数组之外唯一一种由程序员定义的类型,数组有自己特殊的语法。C++ 中的结构体被定义为类的一种特殊形式默认情况下,其成员是全局可见的)。Java 没有struct的特殊概念;它的程序员在所有情况下都使用类。C# 和 Swift 对类型的变量使用引用模型,对结构类型的变量使用值模型。在这些语言中,结构体不支持继承。为简单起见,在大部分讨论中,我们将使用术语“记录”来指代所有这些语言中的相关结构体。

Record types allow related data of heterogeneous types to be stored and manipulated together. Originally introduced by Cobol, records also appeared in Algol 68, which called them structures, and introduced the keyword struct. Many modern languages, including C and its descendants, employ the Algol terminology. Fortran 90 simply calls its records “types”: they are the only form of programmer-defined type other than arrays, which have their own special syntax. Structures in C++ are defined as a special form of class (one in which members are globally visible by default). Java has no distinguished notion of struct; its programmers use classes in all cases. C# and Swift use a reference model for variables of class types, and a value model for variables of struct types. In these languages, structs do not support inheritance. For the sake of simplicity, we will use the term “record” in most of our discussion to refer to the relevant construct in all these languages.

8.1.1 语法和操作

8.1.1 Syntax and Operations

例 8.1

Example 8.1

交流结构

A C struct

在 C 语言中,一条简单记录可能定义如下:

In C, a simple record might be defined as follows:

结构元素 {

struct element {

 字符名称[2];

 char name[2];

 int原子序数;

 int atomic_number;

 双原子量;

 double atomic_weight;

 _Bool金属;

 _Bool metallic;

};

};

例 8.2

Example 8.2

访问记录字段

Accessing record fields

每个记录组件称为一个字段。为了引用记录的给定字段,大多数语言使用“点”符号:

Each of the record components is known as a field. To refer to a given field of a record, most languages use “dot” notation:

铜元素;

element copper;

const double AN = 6.022e23; /* 阿伏伽德罗常数 */

const double AN = 6.022e23; /* Avogadro's number */

铜.名称[0] = 'C'; 铜.名称[1] = 'u';

copper.name[0] = 'C'; copper.name[1] = 'u';

双原子 = 质量/铜.原子重量 * AN;

double atoms = mass / copper.atomic_weight * AN;

在 Fortran 90 中,人们会说copper%namecopper%atomic_weight。Cobol 会反转字段和记录名称的顺序:name of copperatomic_weight of copper。在 Common Lisp 中,人们会说(element-name copper)(element-atomic_weight copper)。■

In Fortran 90 one would say copper%name and copper%atomic_weight. Cobol reverses the order of the field and record names: name of copper and atomic_weight of copper. In Common Lisp, one would say (element-name copper) and (element-atomic_weight copper). ■

例 8.3

Example 8.3

嵌套记录

Nested records

大多数语言允许记录定义嵌套。在 C 语言中:

Most languages allow record definitions to be nested. Again in C:

结构矿石 {

struct ore {

 字符名称[30];

 char name[30];

 结构 {

 struct {

  字符名称[2];

  char name[2];

  int原子序数;

  int atomic_number;

  双原子量;

  double atomic_weight;

  _Bool金属;

  _Bool metallic;

 } 元素_产量;

 } element_yielded;

};

};

或者也可以说,

Alternatively, one could say

结构矿石 {

struct ore {

 字符名称[30];

 char name[30];

 结构元素element_yielded;

 struct element element_yielded;

};

};

在 Fortran 90 和 Common Lisp 中,只允许第二种选择:记录字段可以有记录类型,但声明不能在词汇上嵌套。嵌套记录的命名很简单:C 中的malachite.element_yielded.atomic_number ; Cobol 中的atomic_number of element_yielded of malachite ; Common Lisp 中的(element-atomic_number (ore-element_yielded malachite)) 。■

In Fortran 90 and Common Lisp, only the second alternative is permitted: record fields can have record types, but the declarations cannot be lexically nested. Naming for nested records is straightforward: malachite.element_yielded.atomic_number in C;atomic_number of element_yielded of malachite in Cobol; (element-atomic_number (ore-element_yielded malachite)) in Common Lisp. ■

例 8.4

Example 8.4

OCaml 记录和元组

OCaml records and tuples

如示例 7.17所示,ML 及其相关语言与大多数语言不同,它们指定记录字段的顺序无关紧要。OCaml 记录值{name = “Cu”; atomic_number = 29; atomic_weight = 63.546; metall = true}与值{atomic_number = 29; name = “Cu”; atomic_weight = 63.546; metall = true}相同— 它们将测试相等性。

As noted in Example 7.17, ML and its relatives differ from most languages in specifying that the order of record fields is insignificant. The OCaml record value {name = “Cu”; atomic_number = 29; atomic_weight = 63.546; metallic = true} is the same as the value {atomic_number = 29; name = “Cu”; atomic_weight = 63.546; metallic = true}—they will test true for equality.

OCaml 的元组,我们在7.2.4 节中简要提到过,并将在11.4.3 节中再次讨论,类似于字段有序但未命名的记录在另一种主要的 ML 方言 SML 中,这种相似性实际上是等价的:元组被定义为字段名称为小整数的记录的语法糖。在 SML 中,值(“Cu”, 29)、{1 = “Cu”, 2 = 29}{2 = 29, 1 = “Cu”}都将测试为相等。■

OCaml's tuples, which we mentioned briefly in Section 7.2.4, and will visit again in Section 11.4.3, resemble records whose fields are ordered, but unnamed. In SML, the other leading ML dialect, the resemblance is actually equivalence: tuples are defined as syntactic sugar for records whose field names are small integers. The values (“Cu”, 29), {1 = “Cu”, 2 = 29}, and {2 = 29, 1 = “Cu”} will all test true for equality in SML. ■

8.1.2 内存布局及其影响

8.1.2 Memory Layout and Its Impact

记录的字段通常存储在内存中的相邻位置。在其符号表中,编译器会跟踪每个记录类型中每个字段的偏移量。当需要访问字段时,编译器通常会生成带有位移寻址的加载或存储指令。对于本地对象,基址寄存器通常是帧指针;位移是记录与寄存器的偏移量与字段在记录中的偏移量之和。

The fields of a record are usually stored in adjacent locations in memory. In its symbol table, the compiler keeps track of the offset of each field within each record type. When it needs to access a field, the compiler will often generate a load or store instruction with displacement addressing. For a local object, the base register is typically the frame pointer; the displacement is then the sum of the record's offset from the register and the field's offset within the record.

例 8.5

Example 8.5

记录类型的内存布局

Memory layout for a record type

图 8.1显示了我们的元素类型在 32 位机器上的可能布局。由于name字段只有两个字符长,因此它在内存中占用两个字节。由于atomic_number是一个整数,并且必须(在大多数机器上)字对齐,因此在name的末尾和atomic_number的开头之间有一个两字节的“空洞”。同样,由于布尔变量(在大多数语言实现中)占用一个字节,因此在metal字段的末尾和下一个对齐位置之间有三个字节的空白。在元素数组中,大多数编译器会为数组的每个成员分配 20 个字节。■

A likely layout for our element type on a 32-bit machine appears in Figure 8.1. Because the name field is only two characters long, it occupies two bytes in memory. Since atomic_number is an integer, and must (on most machines) be word-aligned, there is a two-byte “hole” between the end of name and the beginning of atomic_number. Similarly, since Boolean variables (in most language implementations) occupy a single byte, there are three bytes of empty space between the end of the metallic field and the next aligned location. In an array of elements, most compilers would devote 20 bytes to every member of the array. ■

编号08-01-9780124104099
图 8.1 32 位机器上元素类型对象的内存布局 。对齐限制导致阴影“洞”。

设计与实现

Design & Implementation

8.1 C 和 C++ 中的结构标记和typedef

8.1 Struct tags and typedef in C and C+ +

C 类型系统的一个特点是struct标记并不完全是类型名称。在示例 8.1中,类型名称是两个单词的短语struct element 。在示例 8.3中,我们使用此名称声明第二个结构体的element_yielded字段。要获得一个单词的名称,可以说typedef struct element element_t,甚至typedef struct element element:struct 标记和typedef名称具有单独的名称空间,因此可以在每个中使用相同的名称。C++ 通过允许将 struct 标记用作不带struct前缀的类型名称来消除这种特性;实际上,它隐式执行typedef

One of the peculiarities of the C type system is that struct tags are not exactly type names. In Example 8.1, the name of the type is the two-word phrase struct element. We used this name to declare the element_yielded field of the second struct in Example 8.3. To obtain a one-word name, one can say typedef struct element element_t, or even typedef struct element element: struct tags and typedef names have separate name spaces, so the same name can be used in each. C++ eliminates this idiosyncrasy by allowing the struct tag to be used as a type name without the struct prefix; in effect, it performs the typedef implicitly.

例 8.6

Example 8.6

嵌套记录作为值

Nested records as values

在具有变量值模型的语言中,嵌套记录自然嵌入在父记录中,它们在父记录中充当具有字或双字对齐的大字段。在具有变量引用模型的语言中,记录类型的字段通常引用另一个位置的数据。差异不仅是内存布局的问题,也是语义的问题。我们可以在图 8.2中看到差异。在 C 语言中,使用变量值模型,数据的布局如图顶部所示。在下面的代码中,使用图顶部的声明,将 s1 赋值s2复制嵌入的T

In a language with a value model of variables, nested records are naturally embedded in the parent record, where they function as large fields with word or double-word alignment. In a language with a reference model of variables, fields of record type are typically references to data in another location. The difference is a matter not only of memory layout, but also of semantics. We can see the difference in Figure 8.2. In C, with a value model of variables, data is laid out as shown at the top of the figure. In the following code, using the declarations at the top of the figure, the assignment of s1 into s2 copies the embedded T:

08-02-9780124104099
图 8.2 C(顶部)和 Java(底部)中嵌套结构(类)的内存布局 。此布局反映了这样一个事实:n 在 C 中是嵌入值,但在 Java 中是引用。我们在这里假设整数和指针的大小相等。

结构 S s1;

struct S s1;

结构 S s2;

struct S s2;

s1.nj = 0;

s1.n.j = 0;

s2=s1;

s2 = s1;

s2.nj = 7;

s2.n.j = 7;

printf(“%d\n”, s1.nj); /* 打印 0 */

printf(“%d\n”, s1.n.j); /* prints 0 */

例 8.7

Example 8.7

嵌套记录作为引用

Nested records as references

相比之下,在 Java 中,由于变量采用引用模型,因此数据的布局如图底部所示。在下面的代码中,使用图底部的声明,将 s1 赋值s2复制引用,因此s2.nj是s1.nj的别名:

In Java, by contrast, with a reference model of variables, data is laid out as shown at the bottom of the figure. In the following code, using the declarations at the bottom of the figure, assignment of s1 into s2 copies only the reference, so s2.n.j is an alias for s1.n.j:

S s1 = 新 S();

S s1 = new S();

s1.n = new T(); // 字段初始化为 0

s1.n = new T(); // fields initialized to 0

s2=s1;

S s2 = s1;

s2.nj = 7;

s2.n.j = 7;

System.out.println(s1.nj); // 打印 7

System.out.println(s1.n.j); // prints 7

例 8.8

Example 8.8

打包类型的布局

Layout of packed types

一些语言和实现允许程序员指定应打包的记录类型(或数组、集合或文件类型)在 Ada 中,可以使用以下指令:

A few languages and implementations allow the programmer to specify that a record type (or an array, set, or file type) should be packed. In Ada, one uses a pragma:

类型元素 = 记录

type element = record

 

 

结尾;

end;

pragma Pack(元素);

pragma Pack(element);

使用gcc进行编译时,使用一个属性

When compiling with gcc, one uses an attribute:

结构 __attribute__ ((__packed__)) 元素 {

struct __attribute__ ((__packed__)) element {

 

 

}

}

Ada 语法是该语言的内置语法;gcc语法是 GNU 扩展。无论哪种情况,该指令都要求编译器优化空间而不是速度。通常,编译器将通过简单地“将字段推到一起”来实现没有空洞的打包记录。但是,要访问未对齐的字段,它必须发出多指令序列,从内存中检索字段的各个部分,然后在寄存器中重新组装它们。图 8.3显示了我们元素类型的可能打包布局(同样适用于 32 位机器)。它的长度为 15 个字节。打包元素记录数组可能会为数组的每个成员分配 16 个字节;也就是说,它会对齐每个元素。打包记录的打包数组可能只为每个成员分配 15 个字节;只有每四个元素会对齐。■

The Ada syntax is built into the language; the gcc syntax is a GNU extension. In either case, the directive asks the compiler to optimize for space instead of speed. Typically, a compiler will implement a packed record without holes, by simply “pushing the fields together.” To access a nonaligned field, however, it will have to issue a multi-instruction sequence that retrieves the pieces of the field from memory and then reassembles them in a register. A likely packed layout for our element type (again for a 32-bit machine) appears in Figure 8.3. It is 15 bytes in length. An array of packed element records would probably devote 16 bytes to each member of the array; that is, it would align each element. A packed array of packed records might devote only 15 bytes to each; only every fourth element would be aligned. ■

08-03-9780124104099
图 8.3 打包 元素 记录的可能内存布局。atomic_number和atomic_weight字段是非对齐的,并且只能通过多指令序列进行读取或写入(在大多数机器上)。

例 8.9

Example 8.9

记录的分配和比较

Assignment and comparison of records

大多数语言允许通过一次操作将一个值分配给整条记录:

Most languages allow a value to be assigned to an entire record in a single operation:

我的元素:=铜;

my_element := copper;

Ada 还允许比较记录的相等性(如果 my_element = copper 则...)。许多其他语言(包括 C 及其后续语言)支持赋值但不支持相等性测试,尽管 C++ 允许程序员为各个记录类型定义后者。■

Ada also allows records to be compared for equality (if my_element = copper then …). Many other languages (including C and its successors) support assignment but not equality testing, though C++ allows the programmer to define the latter for individual record types. ■

对于小记录,复制和比较都可以逐个字段地在线执行。对于较长的记录,我们可以通过推迟到库例程来显著节省代码空间。block_copy 例程可以将源地址、目标地址和长度作为参数,但类似的block_compare例程将无法处理孔中含有不同(垃圾)数据的记录。一种解决方案是安排所有孔都包含一些可预测的值(例如零),但这需要在每个细化点都编写代码。另一种方法是让编译器为每种记录类型生成自定义的逐个字段比较例程。将调用不同的例程来比较不同类型的记录。

For small records, both copies and comparisons can be performed in-line on a field-by-field basis. For longer records, we can save significantly on code space by deferring to a library routine. A block_copy routine can take source address, destination address, and length as arguments, but the analogous block_compare routine would fail on records with different (garbage) data in the holes. One solution is to arrange for all holes to contain some predictable value (e.g., zero), but this requires code at every elaboration point. Another is to have the compiler generate a customized field-by-field comparison routine for every record type. Different routines would be called to compare records of different types.

例 8.10

Example 8.10

通过对字段进行排序来最小化漏洞

Minimizing holes by sorting fields

除了使比较复杂之外,记录中的空洞还会浪费空间。打包可以消除空洞,但可能会严重影响访问时间。一些编译器采用的折衷方法是根据对齐约束的大小对记录的字段进行排序。所有字节对齐的字段可能排在最前面,然后是半字对齐的字段、字对齐的字段和(如果硬件需要)双字对齐的字段。对于我们的元素类型,生成的重新排列如图8.4所示。■

In addition to complicating comparisons, holes in records waste space. Packing eliminates holes, but at potentially heavy cost in access time. A compromise, adopted by some compilers, is to sort a record's fields according to the size of their alignment constraints. All byte-aligned fields might come first, followed by any half-word aligned fields, word-aligned fields, and (if the hardware requires) double-word-aligned fields. For our element type, the resulting rearrangement is shown in Figure 8.4. ■

08-04-9780124104099
图 8.4 重新排列记录字段以最小化空洞。通过根据字段对齐约束的大小对其进行排序,编译器可以最小化分配给空洞的空间,同时保持字段对齐。

在大多数情况下,字段的重新排序纯粹是一个实现问题:只要记录类型的所有实例都以相同的方式重新排序,程序员就不需要知道这一点。例外发生在系统程序中,系统程序有时会“查看”数据类型的实现,期望它将以特定的方式映射到内存中。例如,内核程序员可能依靠特定的布局策略来定义记录模仿特定以太网设备的内存映射控制寄存器的组织。C 和 C++ 主要是为系统程序设计的,它们保证结构体的字段按照声明的顺序分配。保证第一个字段具有硬件对任何类型的要求的最粗对齐(通常是四字节或八字节边界)。后续字段具有其类型的自然对齐。Fortran 90 允许程序员指定字段不得重新排序;如果没有这样的规范,编译器可以选择自己的顺序。为了适应系统程序,Ada、C 和 C++ 都允许程序员精确指定记录的每个字段应占用多少位。如果“packed”指令本质上是程序员优先级的非约束性指示,则字段声明的位长度是汇编级布局的约束性规范。

In most cases, reordering of fields is purely an implementation issue: the programmer need not be aware of it, so long as all instances of a record type are reordered in the same way. The exception occurs in systems programs, which sometimes “look inside” the implementation of a data type with the expectation that it will be mapped to memory in a particular way. A kernel programmer, for example, may count on a particular layout strategy in order to define a record that mimics the organization of memory-mapped control registers for a particular Ethernet device. C and C++, which are designed in large part for systems programs, guarantee that the fields of a struct will be allocated in the order declared. The first field is guaranteed to have the coarsest alignment required by the hardware for any type (generally a four- or eight-byte boundary). Subsequent fields have the natural alignment for their type. Fortran 90 allows the programmer to specify that fields must not be reordered; in the absence of such a specification the compiler can choose its own order. To accommodate systems programs, Ada, C, and C++ all allow the programmer to specify exactly how many bits to devote to each field of a record. Where a “packed” directive is essentially a nonbinding indication of the programmer's priorities, bit lengths on field declarations are a binding specification of assembly-level layout.

设计与实现

Design & Implementation

8.2 记录字段的顺序

8.2 The order of record fields

记录字段顺序问题与实现权衡密切相关:记录中的空洞会浪费空间,但对齐可以加快访问速度。如果空洞包含垃圾,我们就无法通过循环遍历字或字节来比较记录,但清零空洞会浪费时间和代码空间。可预测的布局对于在“系统”语言中镜像硬件结构非常重要,但如果我们可以将经常访问的字段组合在一起,使它们位于同一个缓存行中,则重组可能对大型记录有利。

Issues of record field order are intimately tied to implementation tradeoffs: Holes in records waste space, but alignment makes for faster access. If holes contain garbage we can't compare records by looping over words or bytes, but zeroing out the holes would incur costs in time and code space. Predictable layout is important for mirroring hardware structures in “systems” languages, but reorganization may be advantageous in large records if we can group frequently accessed fields together, so they lie in the same cache line.

8.1.3 变体记录(并集)

8.1.3 Variant Records (Unions)

例 8.11

Example 8.11

C 语言中的 union

A union in C

20 世纪 60 年代和 70 年代的编程语言是在内存严重受限的时代设计的。许多编程语言允许程序员指定某些变量(大概是永远不会同时使用的变量)应该“堆叠”在一起,共享内存中的相同字节。C 的语法深受 Algol 68 的影响,看起来非常像一个结构体:

Programming languages of the 1960s and 1970s were designed in an era of severe memory constraints. Many allowed the programmer to specify that certain variables (presumably ones that would never be used at the same time) should be allocated “on top of” one another, sharing the same bytes in memory. C's syntax, heavily influenced by Algol 68, looks very much like a struct:

联合 {

union {

 int 我;

 int i;

 双 d;

 double d;

 _Bool b;

 _Bool b;

};

};

此联合体的整体大小将是其最大成员(可能是d )的大小。d 的哪些字节将与ib重叠取决于实现,并且可能受类型的相对大小、对齐约束和硬件的字节序影响。■

The overall size of this union would be that of its largest member (presumably d). Exactly which bytes of d would be overlapped by i and b is implementation dependent, and presumably influenced by the relative sizes of types, their alignment constraints, and the endian-ness of the hardware. ■

在实践中,联合主要有两个用途。第一个用途出现在系统程序中,联合允许在同一组字节中解释在不同时间以不同的方式实现。典型示例出现在内存管理中,其中存储有时可能被视为未分配空间(可能需要“清零”),有时被视为簿记信息(长度和标头字段,用于跟踪空闲和已分配的块),有时被视为用户分配的任意类型的数据。虽然非转换类型转换(7.2.1 节)可用于实现堆管理例程,但联合更能表明程序员的意图:这些位不会被重新解释,而是用于独立目的。1

In practice, unions have been used for two main purposes. The first arises in systems programs, where unions allow the same set of bytes to be interpreted in different ways at different times. The canonical example occurs in memory management, where storage may sometimes be treated as unallocated space (perhaps in need of “zeroing out”), sometimes as bookkeeping information (length and header fields to keep track of free and allocated blocks), and sometimes as user-allocated data of arbitrary type. While nonconverting type casts (Section 7.2.1) can be used to implement heap management routines, unions are a better indication of the programmer's intent: the bits are not being reinterpreted, they are being used for independent purposes.1

例 8.12

Example 8.12

变体记录的动机

Motivation for variant records

联合的第二个历史用途是用于表示记录中的备选字段集。例如,表示员工的记录可能有几个公共字段(姓名、地址、电话、部门、身份证号码)和各种其他字段,具体取决于该员工是领薪水、按小时还是按顾问工作。传统的 C 联合用于此目的时很不方便。Pascal 及其后续版本的变体记录中出现了一种更简洁的语法,允许程序员指定记录中的某些字段应在内存中重叠。类似的功能以匿名联合的形式添加到 C11 和 C++11 中。■

The second, historical purpose for unions was to represent alternative sets of fields within a record. A record representing an employee, for example, might have several common fields (name, address, phone, department, ID number) and various other fields depending on whether the person in question works on a salaried, hourly, or consulting basis. Traditional C unions were awkward when used for this purpose. A much cleaner syntax appeared in the variant records of Pascal and its successors, which allow the programmer to specify that certain fields within a record should overlap in memory. Similar functionality was added to C11 and C++11 in the form of anonymous unions. ■

08-02-9780124104099 更深入地

IN MORE DEPTH

我们在配套网站上更详细地讨论了联合和变体记录。我们考虑的主题包括语法、安全性和内存布局问题。安全性是一个特别令人担忧的问题:非转换类型转换允许程序员明确地规避语言的类型系统,而对联合的简单实现很容易意外地做到这一点。Ada 对联合和变体记录的使用施加了限制,允许编译器静态地验证所有程序是否都是类型安全的。我们还注意到,在大多数情况下,面向对象语言中的继承为类型安全的变体记录提供了一种有吸引力的替代方案。这一观察结果在很大程度上解释了大多数较新的语言中省略联合和变体记录的原因。

We discuss unions and variant records in more detail on the companion site. Topics we consider include syntax, safety, and memory layout issues. Safety is a particular concern: where nonconverting type casts allow a programmer to circumvent the language's type system explicitly, a naive realization of unions makes it easy to do so by accident. Ada imposes limits on the use of unions and variant records that allow the compiler to verify, statically, that all programs are type-safe. We also note that inheritance in object-oriented languages provides an attractive alternative to type-safe variant records in most cases. This observation largely accounts for the omission of unions and variant records from most more recent languages.

08-01-9780124104099检查你的理解

Check Your Understanding

1. C 中的 结构标签是什么?它们与类型名称有何关系?它们在 C++ 中有何变化?

1. What are struct tags in C? How are they related to type names? How did they change in C++?

2.  ML 的记录与大多数其他语言的记录有何不同?

2. How do the records of ML differ from those of most other languages?

3. 讨论记录中“漏洞”的意义。为什么会出现漏洞?漏洞会导致什么问题?

3. Discuss the significance of “holes” in records. Why do they arise? What problems do they cause?

4. 为什么对于记录来说,实现赋值比比较更容易?

4. Why is it easier to implement assignment than comparison for records?

5. 什么是包装?它的优点和缺点是什么?

5. What is packing ? What are its advantages and disadvantages?

6. 编译器为什么可能对记录的字段重新排序?这可能导致什么问题?

6. Why might a compiler reorder the fields of a record? What problems might this cause?

7. 简要描述联合/变体记录的两个目的。

7. Briefly describe two purposes for unions/variant records.

8.2 数组

8.2 Arrays

数组是最常见和最重要的复合数据类型。从 Fortran I 开始,它们已成为几乎所有高级语言的基本组成部分。与将不同类型的相关字段分组的记录不同,数组通常是同质的。从语义上讲,它们可以被认为是从索引类型组件元素类型的映射。某些语言(例如 Fortran)要求索引类型为整数;许多语言允许它是任何离散类型。某些语言(例如 Fortran 77)要求数组的元素类型为标量。大多数(包括 Fortran 90)允许任何元素类型。

Arrays are the most common and important composite data types. They have been a fundamental part of almost every high-level language, beginning with Fortran I. Unlike records, which group related fields of disparate types, arrays are usually homogeneous. Semantically, they can be thought of as a mapping from an index type to a component or element type. Some languages (e.g., Fortran) require that the index type be integer; many languages allow it to be any discrete type. Some languages (e.g., Fortran 77) require that the element type of an array be scalar. Most (including Fortran 90) allow any element type.

一些语言(尤其是脚本语言,但也包括一些较新的命令式语言,包括 Go 和 Swift)允许非离散索引类型。 生成的关联数组通常必须用哈希表或搜索树实现;我们将在14.4.3 节中考虑它们。 关联数组也类似于许多面向对象语言的标准库支持的字典映射类型。 在 C++ 中,运算符重载允许这些类型使用传统的类似数组的语法。 出于本章的目的,我们假设数组索引是离散的。 这允许一种(更高效的)连续分配方案,将在第 8.2.3 节中描述。​​ 我们还假设数组是密集的——它们的大部分元素不等于零或其他默认值。 替代方案——稀疏数组——出现在许多重要的科学问题中。 对于这些问题,库(或在极少数情况下,语言本身)可能支持明确枚举非默认值的替代实现。

Some languages (notably scripting languages, but also some newer imperative languages, including Go and Swift) allow nondiscrete index types. The resulting associative arrays must generally be implemented with hash tables or search trees; we consider them in Section 14.4.3. Associative arrays also resemble the dictionary or map types supported by the standard libraries of many object-oriented languages. In C++, operator overloading allows these types to use conventional array-like syntax. For the purposes of this chapter, we will assume that array indices are discrete. This admits a (much more efficient) contiguous allocation scheme, to be described in Section 8.2.3. We will also assume that arrays are dense—that a large fraction of their elements are not equal to zero or some other default value. The alternative—sparse arrays—arises in many important scientific problems. For these, libraries (or, in rare cases, the language itself) may support an alternative implementation that explicitly enumerates only the non-default values.

8.2.1 语法和操作

8.2.1 Syntax and Operations

大多数语言通过在数组名称后附加下标(通常用方括号分隔)来引用数组元素:A[3]。少数语言(尤其是 Fortran 和 Ada)改用括号:A(3)

Most languages refer to an element of an array by appending a subscript—usually delimited by square brackets—to the name of the array: A[3]. A few languages— notably Fortran and Ada—use parentheses instead: A(3).

例 8.13

Example 8.13

数组声明

Array declarations

在某些语言中,可以通过在声明标量的语法后附加下标符号来声明数组。在 C 语言中:

In some languages one declares an array by appending subscript notation to the syntax that would be used to declare a scalar. In C:

字符上[26];

char upper[26];

在 Fortran 中:

In Fortran:

角色,尺寸 (1:26) :: 上部

character, dimension (1:26) :: upper

字符(26)大写!简写符号

character (26) upper ! shorthand notation

在 C 语言中,索引范围的下限始终为零:n元素数组的索引为 0 … n​​ − 1。在 Fortran 语言中,索引范围的下限默认为 1。Fortran 90 允许根据需要指定不同的下限,使用上面两个声明中第一个所示的符号。

In C, the lower bound of an index range is always zero: the indices of an n-element array are 0 …n − 1. In Fortran, the lower bound of the index range is one by default. Fortran 90 allows a different lower bound to be specified if desired, using the notation shown in the first of the two declarations above.

许多 Algol 后代使用数组构造函数。例如,在 Ada 中,有人可能会说

Many Algol descendants use an array constructor instead. In Ada, for example, one might say

upper:字符数组(字符范围'a'..'z');

upper : array (character range 'a'..'z') of character;

例 8.14

Example 8.14

多维数组

Multidimensional arrays

大多数语言都可以轻松声明多维数组:

Most languages make it easy to declare multidimensional arrays:

mat:long_float 数组(1..10,1..10);——Ada

mat : array (1..10, 1..10) of long_float;-- Ada

实数,维度 (10,10) :: mat ! Fortran

real, dimension (10,10) :: mat ! Fortran

在某些语言中,还可以通过在同一声明中多次使用数组构造函数来声明多维数组。例如,在 Modula-3 中,

In some languages, one can also declare a multidimensional array by using the array constructor more than once in the same declaration. In Modula-3, for example,

VAR mat : ARRAY [1..10], [1..10] OF REAL;

VAR mat : ARRAY [1..10], [1..10] OF REAL;

是语法糖

is syntactic sugar for

VAR mat:数组[1..10],数组[1..10],实数;

VAR mat : ARRAY [1..10] OF ARRAY [1..10] OF REAL;

并且mat[3, 4]是mat[3][4]的语法糖。■

and mat[3, 4] is syntactic sugar for mat[3][4]. ■

例 8.15

Example 8.15

多维数组与组合数组

Multidimensional vs built-up arrays

相比之下,在 Ada 中,

In Ada, by contrast,

mat1:long_float 数组(1..10,1..10);

mat1 : array (1..10, 1..10) of long_float;

不同于

is not the same as

类型向量是long_float数组(整数范围<>);

type vector is array (integer range <>) of long_float;

类型矩阵是向量(1..10)的数组(整数范围<>);

type matrix is array (integer range <>) of vector (1..10);

mat2:矩阵(1..10);

mat2 : matrix (1..10);

变量mat1是一个二维数组;mat2是一个一维数组数组。使用前者声明,我们可以将单个实数作为mat1(3, 4)访问;使用后者,我们必须说mat2(3)(4)。二维数组可以说更优雅,但数组数组支持其他操作:它允许我们单独命名mat2的行(mat2(3)是一个 10 元素的一维数组),并且它允许我们进行切片,如上所述(mat2(3)(2..6)是一个由五元素组成的实数数组;mat2(3..7)是一个由十元素组成的五元素数组)。■

Variable mat1 is a two-dimensional array; mat2 is an array of one-dimensional arrays. With the former declaration, we can access individual real numbers as mat1(3, 4); with the latter we must say mat2(3)(4). The two-dimensional array is arguably more elegant, but the array of arrays supports additional operations: it allows us to name the rows of mat2 individually (mat2(3) is a 10-element, single-dimensional array), and it allows us to take slices, as discussed below (mat2(3)(2..6) is a five-element array of real numbers; mat2(3..7) is a five-element array of ten-element arrays). ■

例 8.16

Example 8.16

C 中的数组的数组

Arrays of arrays in C

在 C 语言中,还必须声明一个数组数组,并使用双下标表示法,但是 C 的指针和数组的集成(将在第8.5.1 节中讨论)意味着不支持切片:

In C, one must also declare an array ofarrays, and use two-subscript notation, but C's integration of pointers and arrays (to be discussed in Section 8.5.1) means that slices are not supported:

双垫[10][10];

double mat[10][10];

根据此定义,mat[3][4]表示数组的一个单独元素,但mat[3]表示一个引用,根据上下文,可以是对数组第三行的引用,也可以是对该行的第一个元素的引用。■

Given this definition, mat[3][4] denotes an individual element of the array, but mat[3] denotes a reference, either to the third row of the array or to the first element of that row, depending on context. ■

设计与实现

Design & Implementation

8.3 []是运算符吗?

8.3 Is [] an operator?

C++ 中的关联数组通常通过重载运算符 []来定义。与 C++ 一样,C# 也提供了广泛的运算符重载功能,但它不使用这些功能来支持关联数组。相反,该语言提供了一种特殊的索引器机制,具有自己独特的语法:

Associative arrays in C++ are typically defined by overloading operator[]. C#, like C++, provides extensive facilities for operator overloading, but it does not use these facilities to support associative arrays. Instead, the language provides a special indexer mechanism, with its own unique syntax:

类目录 {

class directory {

 Hashtable 表;//来自标准库

 Hashtable table; // from standard library

 

 

 public directory() { // 构造函数

 public directory() { // constructor

  表 = 新的 Hashtable();

  table = new Hashtable();

 }

 }

 

 

 public string this[string name] { // 索引器方法

 public string this[string name] { // indexer method

  得到 {

  get {

   返回(字符串)表[名称];

   return (string) table[name];

}

}

  放 {

  set {

   table[name] = value; // 值是隐式的

   table[name] = value; // value is implicitly

} } } // 设置参数

} } } // a parameter of set

目录 d = 新目录();

directory d = new directory();

d[“Jane Doe”] = “234-5678”;

d[“Jane Doe”] = “234-5678”;

Console.WriteLine(d[“Jane Doe”]);

Console.WriteLine(d[“Jane Doe”]);

为什么会有差异?在 C++ 中,operator[]可以返回一个引用(显式的左值),该引用可以在赋值的任意一侧使用(更多信息可参见第 9.3.1 节中的“C++ 中的引用” )。C# 没有类似的左值机制,因此需要单独的方法来获取设置d[“Jane Doe”]的值。

Why the difference? In C++, operator[] can return a reference (an explicit l-value), which can be used on either side of an assignment (further information can be found under “References in C++” in Section 9.3.1). C# has no comparable l-value mechanism, so it needs separate methods to get and set the value of d[“Jane Doe”].

切片和数组操作

Slices and Array Operations

例 8.17

Example 8.17

数组切片操作

Array slice operations

切片或部分是数组的矩形部分。Fortran 90 提供了广泛的切片功能,Go 和许多脚本语言也是如此。图 8.5使用示例 8.14中的mat声明说明了 Fortran 90 中的一些可能性。Ada提供的支持更有限:切片只是一维数组中元素的连续范围。正如我们在示例 8.15中看到的,元素本身可以是数组,但无法通过单个操作沿两个维度提取切片。■

A slice or section is a rectangular portion of an array. Fortran 90 provides extensive facilities for slicing, as do Go and many scripting languages. Figure 8.5 illustrates some of the possibilities in Fortran 90, using the declaration of mat from Example 8.14. Ada provides more limited support: a slice is simply a contiguous range of elements in a one-dimensional array. As we saw in Example 8.15, the elements can themselves be arrays, but there is no way to extract a slice along both dimensions as a single operation. ■

08-05-9780124104099
图 8.5 Fortran 90 中的数组切片(节)。与枚举控制循环头中的值(第 6.5.1 节)非常相似,下标中的a : b : c表示位置aa + ca + 2 c,...到b。如果省略ab,则假定数组的相应边界。如果省略c,则假定为 1。甚至可以使用c的负值来以相反的顺序选择位置。右下角示例的第二个下标中的斜线界定了一个明确的位置列表。

在大多数语言中,数组上允许的唯一操作是选择元素(然后可以将其用于对其类型有效的任何操作)和赋值。少数语言(例如 Ada 和 Fortran 90)允许比较数组是否相等。Ada 允许对元素离散的一维数组进行字典顺序比较:如果A中第一个不等于B中相应元素的元素小于该相应元素,则A < B。Ada还允许将内置逻辑运算符(或、与、异或)应用于布尔数组。

In most languages, the only operations permitted on an array are selection of an element (which can then be used for whatever operations are valid on its type), and assignment. A few languages (e.g., Ada and Fortran 90) allow arrays to be compared for equality. Ada allows one-dimensional arrays whose elements are discrete to be compared for lexicographic ordering: A < B if the first element of A that is not equal to the corresponding element of B is less than that corresponding element. Ada also allows the built-in logical operators (or, and, xor) to be applied to Boolean arrays.

Fortran 90 具有一组非常丰富的数组操作:以整个数组为参数的内置操作。由于 Fortran 使用结构类型等价,因此数组运算符的操作数只需具有相同的元素类型和形状。特别是,相同形状的切片可以在数组操作中混合使用,即使它们被切片的数组具有非常不同的形状。任何内置算术运算符都将数组作为操作数;结果是一个与操作数形状相同的数组,其元素是将运算符应用于相应元素的结果。举一个简单的例子,A + B是一个数组,其每个元素都是AB的相应元素的总和。 Fortran 90 还提供了大量的内在函数或内置函数。其中 60 多个(包括逻辑和位操作、三角学、对数和指数、类型转换和字符串操作)是在标量上定义的,但如果传递数组作为参数,它们也会逐个元素执行操作。例如,函数tan(A)返回一个由A元素的切线组成的数组。许多其他内在函数仅在数组上定义。这些包括搜索和总结,转置,重塑和下标排列。

Fortran 90 has a very rich set of array operations: built-in operations that take entire arrays as arguments. Because Fortran uses structural type equivalence, the operands of an array operator need only have the same element type and shape. In particular, slices of the same shape can be intermixed in array operations, even if the arrays from which they were sliced have very different shapes. Any of the built-in arithmetic operators will take arrays as operands; the result is an array, of the same shape as the operands, whose elements are the result of applying the operator to corresponding elements. As a simple example, A + B is an array each of whose elements is the sum of the corresponding elements of A and B. Fortran 90 also provides a huge collection of intrinsic, or built-in functions. More than 60 of these (including logic and bit manipulation, trigonometry, logs and exponents, type conversion, and string manipulation) are defined on scalars, but will also perform their operation element-wise if passed arrays as arguments. The function tan(A), for example, returns an array consisting of the tangents of the elements of A. Many additional intrinsic functions are defined solely on arrays. These include searching and summarization, transposition, and reshaping and subscript permutation.

Fortran 90 在很大程度上借鉴了 APL 的灵感,APL 是 Iverson 等人在 20 世纪 60 年代初期至中期开发的一种数组操作语言。2 APL主要设计为数组操作的简洁数学符号。它采用了庞大的字符集,这使其难以与传统键盘和文本显示一起使用。它的变量都是数组,许多特殊字符表示数组操作。APL 实现设计用于解释、交互使用。它们最适合“快速而粗略”地解决数学问题。非常强大的运算符与非常简洁的符号相结合,使得 APL 程序非常难以阅读和理解。J 是 APL 的后继者,使用常规字符集。

Fortran 90 draws significant inspiration from APL, an array manipulation language developed by Iverson and others in the early to mid-1960s.2 APL was designed primarily as a terse mathematical notation for array manipulations. It employs an enormous character set that made it difficult to use with traditional keyboards and textual displays. Its variables are all arrays, and many of the special characters denote array operations. APL implementations are designed for interpreted, interactive use. They are best suited to “quick and dirty” solution of mathematical problems. The combination of very powerful operators with very terse notation makes APL programs notoriously difficult to read and understand. J, a successor to APL, uses a conventional character set.

8.2.2 尺寸、边界和分配

8.2.2 Dimensions, Bounds, and Allocation

在上一小节的所有示例中,数组的形状(包括边界)都是在声明中指定的。对于这种静态形状的数组,可以按通常的方式管理存储:对于生存期为整个程序的数组,使用静态分配;对于生存期为子例程调用的数组,使用堆栈分配;对于具有更通用生存期的动态分配数组,使用堆分配。

In all of the examples in the previous subsection, the shape of the array (including bounds) was specified in the declaration. For such static shape arrays, storage can be managed in the usual way: static allocation for arrays whose lifetime is the entire program; stack allocation for arrays whose lifetime is an invocation of a subroutine; heap allocation for dynamically allocated arrays with more general lifetime.

对于形状在制定阶段才知道的数组,或者形状在执行过程中可能发生变化的数组,存储管理更为复杂。对于这些数组,编译器不仅必须安排分配空间,而且还必须在运行时提供形状信息(如果没有这些信息,索引就不可能实现)。一些动态类型语言允许运行时绑定维数和维数边界都是如此。编译型语言可能允许边界是动态的,但通常要求维数是静态的。在阐述时已知形状的本地数组仍可能分配在堆栈中。在执行过程中大小可能发生变化的数组通常必须分配在堆中。

Storage management is more complex for arrays whose shape is not known until elaboration time, or whose shape may change during execution. For these the compiler must arrange not only to allocate space, but also to make shape information available at run time (without such information, indexing would not be possible). Some dynamically typed languages allow run-time binding of both the number and bounds of dimensions. Compiled languages may allow the bounds to be dynamic, but typically require the number of dimensions to be static. A local array whose shape is known at elaboration time may still be allocated in the stack. An array whose size may change during execution must generally be allocated in the heap.

在下面的第一小节中,我们考虑用于在运行时保存形状信息的描述符内幕向量3。然后,我们分别考虑动态形状数组的基于堆栈和基于堆的分配。

In the first subsection below we consider the descriptors, or dope vectors,3 used to hold shape information at run time. We then consider stack- and heap-based allocation, respectively, for dynamic shape arrays.

涂料矢量图

Dope Vectors

在编译期间,符号表会维护程序中每个数组的维度和边界信息。对于每条记录,它都会维护每个字段的偏移量。当数组维度的数量和边界是静态已知时,编译器可以在符号表中查找它们,以便计算数组元素的地址。当这些值不是静态已知时,编译器必须生成代码以在运行时在原始向量中查找它们。

During compilation, the symbol table maintains dimension and bounds information for every array in the program. For every record, it maintains the offset of every field. When the number and bounds of array dimensions are statically known, the compiler can look them up in the symbol table in order to compute the address of elements of the array. When these values are not statically known, the compiler must generate code to look them up in a dope vector at run time.

在一般情况下,dope 向量必须指定每个维度的下限和除最后一个维度之外的每个维度的大小(最后一个维度始终是元素类型的大小,因此是静态已知的)。如果语言实现对数组引用中的越界下标执行动态语义检查,则 dope 向量也可能包含上限。给定下限和大小,上限信息是多余的,但包括它可以避免在运行时重复重新计算。

In the general case a dope vector must specify the lower bound of each dimension and the size of each dimension other than the last (which is always the size of the element type, and will thus be statically known). If the language implementation performs dynamic semantic checks for out-of-bounds subscripts in array references, then the dope vector may contain upper bounds as well. Given lower bounds and sizes, the upper bound information is redundant, but including it avoids the need to recompute repeatedly at run time.

内幕向量的内容在精化时或维度数量或边界发生变化时初始化。在 Fortran 90 等语言中,其形状概念包括维度大小但不包括下限,赋值语句可能不仅需要复制数组的数据,还需要复制内幕向量的内容。

The contents of the dope vector are initialized at elaboration time, or whenever the number or bounds of dimensions change. In a language like Fortran 90, whose notion of shape includes dimension sizes but not lower bounds, an assignment statement may need to copy not only the data of an array, but dope vector contents as well.

在同时提供变量值模型和动态形状数组的语言中,我们必须考虑记录可能包含大小不为静态所知的字段的可能性。在这种情况下,编译器可能不仅将内幕向量用于动态形状数组,还将其用于动态形状记录。记录的内幕向量通常指示每个字段与记录开头的偏移量。

In a language that provides both a value model of variables and arrays of dynamic shape, we must consider the possibility that a record will contain a field whose size is not statically known. In this case the compiler may use dope vectors not only for dynamic shape arrays, but also for dynamic shape records. The dope vector for a record typically indicates the offset of each field from the beginning of the record.

堆栈分配

Stack Allocation

子程序参数和局部变量是动态形状数组最简单的例子。早期版本的 Pascal 要求静态指定所有数组的形状。标准 Pascal 允许动态数组作为子程序参数,其形状在子程序调用时固定。此类参数有时称为一致数组。 除其他外,它们还有助于构建线性代数库,这些库的例程通常必须在任意大小的数组上工作。为了实现这样的数组,编译器安排调用者传递数组的数据和适当的 dope 向量。如果数组在调用者的上下文中是动态形状,则 dope 向量可能已经可用。如果数组在调用者的上下文中是静态形状,则需要在调用之前创建适当的 dope 向量。

Subroutine parameters and local variables provide the simplest examples of dynamic shape arrays. Early versions of Pascal required the shape of all arrays to be specified statically. Standard Pascal allowed dynamic arrays as subroutine parameters, with shape fixed at subroutine call time. Such parameters are sometimes known as conformant arrays. Among other things, they facilitate the construction of linear algebra libraries, whose routines must typically work on arrays of arbitrary size. To implement such an array, the compiler arranges for the caller to pass both the data of the array and an appropriate dope vector. If the array is of dynamic shape in the caller's context, the dope vector may already be available. If the array is of static shape in the caller's context, an appropriate dope vector will need to be created prior to the call.

例 8.18

Example 8.18

C 语言中动态形状的局部数组

Local arrays of dynamic shape in C

Ada 和 C(但不是 C++)支持参数和局部变量的动态形状。除其他外,可以声明局部数组以匹配符合要求的数组参数的形状,从而有助于实现需要临时空间进行计算的算法。图 8.6包含一个 C 语言中的简单示例。函数square接受动态形状的数组参数M,并分配相同动态形状的局部变量T。

Ada and C (though not C++) support dynamic shape for both parameters and local variables. Among other things, local arrays can be declared to match the shape of conformant array parameters, facilitating the implementation of algorithms that require temporary space for calculations. Figure 8.6 contains a simple example in C. Function square accepts an array parameter M of dynamic shape and allocates a local variable T of the same dynamic shape. ■

编号:F08-06-9780124104099
图 8.6 C 中的动态局部数组。函数 square 将矩阵与自身相乘,并用乘积替换原始矩阵。为此,它需要一个与参数形状相同的临时数组。请注意,MT的声明都依赖于参数 n。

例 8.19

Example 8.19

详细数组的堆栈分配

Stack allocation of elaborated arrays

在许多语言中,包括 Ada 和 C,局部数组的形状在精化时就固定了。对于这样的数组,仍然可以将数组的空间放在其子程序的堆栈框架中,但需要额外的间接级别(参见图 8.7)。为了确保每个局部对象都可以使用已知的帧指针偏移量找到,我们将堆栈框架分为固定大小部分可变大小部分。静态已知大小的对象放在固定大小部分。直到精化时才知道大小的对象放在可变大小部分,指向它的指针和内部向量一起放在固定大小部分。如果数组的精化隐藏在嵌套块中,则编译器会延迟分配空间(即更改堆栈指针),直到进入该块。它仍会为指针和内部向量分配空间在进入子程序本身时,局部变量中记录的记录。动态形状的记录以类似的方式处理。■

In many languages, including Ada and C, the shape of a local array becomes fixed at elaboration time. For such arrays it is still possible to place the space for the array in the stack frame of its subroutine, but an extra level of indirection is required (see Figure 8.7). In order to ensure that every local object can be found using a known offset from the frame pointer, we divide the stack frame into a fixed-size part and a variable-size part. An object whose size is statically known goes in the fixed-size part. An object whose size is not known until elaboration time goes in the variable-size part, and a pointer to it, together with a dope vector, goes in the fixed-size part. If the elaboration of the array is buried in a nested block, the compiler delays allocating space (i.e., changing the stack pointer) until the block is entered. It still allocates space for the pointer and the dope vector among the local variables when the subroutine itself is entered. Records of dynamic shape are handled in a similar way. ■

编号:F08-07-9780124104099
图 8.7 数组的精化时分配。这里M是一个方形二维数组,其边界由运行时传递给 foo 的参数确定。编译器安排指向M 的指针和一个 dope 向量驻留在距框架指针M的静态偏移处。不能将其放置在其他局部变量中,因为这会阻止框架中较高的变量具有静态偏移。可以轻松容纳其他可变大小的数组或记录。

例 8.20

Example 8.20

Fortran 90 中的精细数组

Elaborated arrays in Fortran 90

Fortran 90 允许在详细说明之后指定数组的边界,但一旦定义了这些边界,就不允许更改它们:

Fortran 90 allows specification of the bounds of an array to be delayed until after elaboration, but it does not allow those bounds to change once they have been defined:

真实,维度(:,:),可分配:: mat

real, dimension (:,:), allocatable :: mat

 ! mat 是二维的,但边界未指定

 ! mat is two-dimensional, but with unspecified bounds

分配(mat(a:b,0:m-1))

allocate (mat (a:b, 0:m-1))

 ! 第一维的边界为 a..b;第二维的边界为 0..m-1

 ! first dimension has bounds a..b; second has bounds 0..m-1

解除分配(mat)

deallocate (mat)

 ! 实现现在可以自由回收 mat 的空间

 ! implementation is now free to reclaim mat's space

执行allocate语句可以视为嵌套块中动态形状数组的详细说明。执行deallocate语句可以视为如果堆栈中除指定数组之外没有其他数组,则将其视为嵌套块的末尾(恢复前一个堆栈指针)。或者,动态形状数组可以在堆中分配,如下一小节所述。■

Execution of an allocate statement can be treated like the elaboration of a dynamic shape array in a nested block. Execution of a deallocate statement can be treated like the end of the nested block (restoring the previous stack pointer) if there are no other arrays beyond the specified one in the stack. Alternatively, dynamic shape arrays can be allocated in the heap, as described in the following subsection. ■

堆分配

Heap Allocation

可以随时改变形状的数组有时被称为完全动态的。由于大小变化通常不按 FIFO 顺序发生,因此堆栈分配不够;完全动态数组必须在堆中分配。

Arrays that can change shape at arbitrary times are sometimes said to be fully dynamic. Because changes in size do not in general occur in FIFO order, stack allocation will not suffice; fully dynamic arrays must be allocated in the heap.

例 8.21

Example 8.21

Java 和 C# 中的动态字符串

Dynamic strings in Java and C#

包括所有主流脚本语言在内的多种语言都允许字符串(字符数组)在处理时间之后改变大小。Java 和 C# 提供了类似的功能(具有类似的实现),但对语义的描述不同:这些语言中的字符串变量是对不可变字符串对象的引用:

Several languages, including all the major scripting languages, allow strings— arrays of characters—to change size after elaboration time. Java and C# provide a similar capability (with a similar implementation), but describe the semantics differently: string variables in these languages are references to immutable string objects:

字符串 s = “short”;// 这是 Java;在 C# 中使用小写的“字符串”
s = s + “ 但很甜蜜”;// + 是连接运算符

这里, String s声明引入了一个字符串变量,我们用对常量字符串“short”的引用来初始化它。在后续的赋值中,+创建了一个新字符串,其中包含旧s和常量“but sweet”的串联;然后设置 s 以引用这个新字符串,而不是旧字符串。请注意,字符数组与 Java 和 C# 中的字符串不同:数组的长度在制定时是固定的,并且其元素可以就地修改。■

Here the declaration String s introduces a string variable, which we initialize with a reference to the constant string “short”. In the subsequent assignment, + creates a new string containing the concatenation of the old s and the constant “but sweet”; s is then set to refer to this new string, rather than the old. Note that arrays of characters are not the same as strings in Java and C#: the length of an array is fixed at elaboration time, and its elements can be modified in place. ■

动态可调整大小的数组(字符串除外)出现在 APL、Common Lisp 和各种脚本语言中。它们也分别受C++、Java 和 C# 库的vector、VectorArrayList类的支持。与 Fortran 90 的可分配数组相比,这些数组可以改变其形状(特别是可以增长),同时保留其当前内容。在许多情况下,增加大小将需要运行时系统分配更大的块,将要保留的任何数据从旧块复制到新块,然后释放旧块。

Dynamically resizable arrays (other than strings) appear in APL, Common Lisp, and the various scripting languages. They are also supported by the vector, Vector, and ArrayList classes of the C++, Java, and C# libraries, respectively. In contrast to the allocate-able arrays of Fortran 90, these arrays can change their shape—in particular, can grow—while retaining their current content. In many cases, increasing the size will require that the run-time system allocate a larger block, copy any data that are to be retained from the old block to the new, and then deallocate the old.

如果完全动态数组的维数是静态已知的,则可以将内部向量与指向数据的指针一起保存在声明数组的子例程的堆栈框架中。如果维数可以改变,则通常必须将内部向量放置在堆块的开头。

If the number of dimensions of a fully dynamic array is statically known, the dope vector can be kept, together with a pointer to the data, in the stack frame of the subroutine in which the array was declared. If the number of dimensions can change, the dope vector must generally be placed at the beginning of the heap block instead.

在没有垃圾收集的情况下,编译器必须在控制从声明全动态数组的子例程返回时安排回收全动态数组占用的空间。堆栈分配的数组的空间当然会通过弹出堆栈自动回收。

In the absence of garbage collection, the compiler must arrange to reclaim the space occupied by fully dynamic arrays when control returns from the subroutine in which they were declared. Space for stack-allocated arrays is of course reclaimed automatically by popping the stack.

8.2.3 内存布局

8.2.3 Memory Layout

大多数语言实现中的数组都存储在内存中的连续位置。在一维数组中,数组的第二个元素紧跟在第一个元素之后存储;第三个元素紧跟在第二个元素之后存储,依此类推。对于记录数组,对齐约束可能会导致连续元素之间出现小空洞。

Arrays in most language implementations are stored in contiguous locations in memory. In a one-dimensional array, the second element of the array is stored immediately after the first; the third is stored immediately after the second, and so forth. For arrays of records, alignment constraints may result in small holes between consecutive elements.

例 8.22

Example 8.22

行主序与列主序数组布局

Row-major vs column-major array layout

对于多维数组,将数组的第一个元素放在数组的第一个内存位置仍然是有意义的。但接下来应该放哪个元素呢?有两个合理的答案,即行主序列主序。在行主序中,内存中连续的位置保存最终下标相差一的元素(行尾除外)。例如, A[2, 4]后面跟着A[2, 5] 。在列主序中,连续的位置保存初始下标相差一的元素:A[2, 4]后面跟着A[3, 4] 。图 8.8针对二维数组说明了这些选项。三维或以上数组的布局类似。Fortran 使用列主序;大多数其他语言使用行主序。 (与 Fran Allen 4的通信表明,最初采用列主序是为了适应 IBM 704 型计算机的控制台调试器和指令集的特性,该语言最初就是在该计算机上实现的。)行主序的优点是它可以轻松地将多维数组定义为子数组的数组,如第8.2.1 节所述。采用列主序时,子数组的元素在内存中不会连续。■

For multidimensional arrays, it still makes sense to put the first element of the array in the array's first memory location. But which element comes next? There are two reasonable answers, called row-major and column-major order. In row-major order, consecutive locations in memory hold elements that differ by one in the final subscript (except at the ends of rows). A[2, 4], for example, is followed by A[2, 5]. In column-major order, consecutive locations hold elements that differ by one in the initial subscript: A[2, 4] is followed by A[3, 4]. These options are illustrated for two-dimensional arrays in Figure 8.8. The layouts for three or more dimensions are analogous. Fortran uses column-major order; most other languages use row-major order. (Correspondence with Fran Allen4 suggests that column-major order was originally adopted in order to accommodate idiosyncrasies of the console debugger and instruction set of the IBM model 704 computer, on which the language was first implemented.) The advantage of row-major order is that it makes it easy to define a multidimensional array as an array of subarrays, as described in Section 8.2.1. With column-major order, the elements of the subarray would not be contiguous in memory. ■

编号:F08-08-9780124104099
图 8.8 二维数组的行主序和列主序内存布局。按行主序,行的元素在内存中是连续的;按列主序,列的元素是连续的。每个数组的第二个缓存行都被涂黑,假设每个元素都是一个 8 字节浮点数;缓存行长 32 字节(常见大小);数组从缓存行边界开始。如果数组的索引从 A[0,0] 到 A[9,9],则在按行主序的情况下,元素 A[0,4] 到 A[0,7] 共享一个缓存行;在按列主序的情况下,元素 A[4,0] 到 A[7,0] 共享一个缓存行。

例 8.23

Example 8.23

阵列布局和缓存性能

Array layout and cache performance

对于使用嵌套循环访问大型多维数组的所有元素的程序来说,行主布局和列主布局之间的差异非常重要。在现代机器上,此类循环的速度往往受内存系统性能的限制,而内存系统性能又在很大程度上取决于缓存的有效性(第 C-5.1 节)。图 8.8显示了行主布局和列主布局的数组的缓存行方向。当代码遍历一个小数组时,它的所有或大部分元素很可能会在嵌套循环结束前保留在缓存中,缓存行的方向无关紧要。然而,对于一个大数组,在遍历早期访问的行很可能会被逐出,为在遍历后期访问的行腾出空间。如果按照连续地址的顺序访问数组元素,那么每次未命中不仅会将所需的元素带入缓存,还会将接下来的几个元素带入缓存。如果元素是缓存行访问的(即,在 Fortran 数组中沿着行访问,或在大多数其他语言中沿着数组的列访问),那么几乎每个访问将导致缓存未命中,从而大大降低代码的性能。在 C 语言中,应该这样写

The difference between row- and column-major layout can be important for programs that use nested loops to access all the elements of a large, multidimensional array. On modern machines the speed of such loops is often limited by memory system performance, which depends heavily on the effectiveness of caching (Section C-5.1). Figure 8.8 shows the orientation of cache lines for row-and column-major layout of arrays. When code traverses a small array, all or most of its elements are likely to remain in the cache through the end of the nested loops, and the orientation of cache lines will not matter. For a large array, however, lines that are accessed early in the traversal are likely to be evicted to make room for lines accessed later in the traversal. If array elements are accessed in order of consecutive addresses, then each miss will bring into the cache not only the desired element, but the next several elements as well. If elements are accessed across cache lines instead (i.e., along the rows of a Fortran array, or the columns of an array in most other languages), then there is a good chance that almost every access will result in a cache miss, dramatically reducing the performance of the code. In C, one should write

for (i = 0; i < N; i++) { /* 行 */

for (i = 0; i < N; i++) { /* rows */

 对于(j = 0; j < N; j++){/*列*/

 for (j = 0; j < N; j++) { /* columns */

  …A[i][j]…

  … A[i][j] …

 }

 }

}

}

在 Fortran 中:

In Fortran:

执行 j = 1, N ! 列

do j = 1, N ! columns

 执行 i = 1, N !行

 do i = 1, N ! rows

  …A(i,j)…

  … A(i, j) …

 结束

 end do

结束做

end do

行指针布局

Row-Pointer Layout

有些语言对某些数组采用连续分配的替代方法。它们不要求数组的行相邻,而是允许它们位于内存中的任何位置,并创建指向行的辅助指针数组。如果数组有超过两个维度,则可以将其分配为指向指针数组的指针数组……这种行指针内存布局需要更多空间在大多数情况下,但有三个潜在的优势。第一个仅具有历史意义:在 1980 年之前设计的机器上,行指针布局有时会导致更快的代码(参见下面有关地址计算的讨论)。其次,行指针布局允许行具有不同的长度,而不必在行末尾留出空间来留出空洞。这种表示有时称为不规则数组。缺少空洞有时可能会抵消指针的增加空间。第三,行指针布局允许程序从预先存在的行(可能分散在整个内存中)构造数组而无需复制。C、C++ 和 C# 为多维数组提供了连续和行指针组织。从技术上讲,连续布局是一个真正的多维数组,而行指针布局是一个指向数组的指针数组。Java 对所有数组都使用行指针布局。

Some languages employ an alternative to contiguous allocation for some arrays. Rather than require the rows of an array to be adjacent, they allow them to lie anywhere in memory, and create an auxiliary array of pointers to the rows. If the array has more than two dimensions, it may be allocated as an array of pointers to arrays of pointers to…. This row-pointer memory layout requires more space in most cases, but has three potential advantages. The first is of historical interest only: on machines designed before about 1980, row-pointer layout sometimes led to faster code (see the discussion of address calculations below). Second, row-pointer layout allows the rows to have different lengths, without devoting space to holes at the ends of the rows. This representation is sometimes called a ragged array. The lack of holes may sometimes offset the increased space for pointers. Third, row-pointer layout allows a program to construct an array from preexisting rows (possibly scattered throughout memory) without copying. C, C++, and C# provide both contiguous and row-pointer organizations for multidimensional arrays. Technically speaking, the contiguous layout is a true multidimensional array, while the row-pointer layout is an array of pointers to arrays. Java uses the row-pointer layout for all arrays.

例 8.24

Example 8.24

连续与行指针数组布局

Contiguous vs row-pointer array layout

到目前为止,C 语言中行指针布局最常见的用途是表示字符串数组。图 8.9中显示了一个典型示例。在此示例(表示星期几)中,行指针内存布局为字符本身消耗 57 个字节(包括每个字符串末尾的NUL字节),再加上为指针消耗 28 个字节(假设是 32 位架构),总共 85 个字节。连续布局替代方案为每一天分配 10 个字节(有足够的空间容纳星期三及其NUL字节),总共 70 个字节。行指针组织所需的额外空间达到 21%。在某些情况下,行指针实际上可以节省空间。例如,用 C 编写的 Java 编译器可能会使用行指针来存储 51 个 Java 关键字和类似单词的文字的字符串表示形式。此数据结构将使用 55 × 4 = 220 字节作为指针(在 32 位机器上),加上 366 字节作为关键字,总共 586 字节。由于最长的关键字(synchronized)需要 13 个字节(包括终止NUL的空间),因此连续的二维数组将消耗 55 × 13 = 715 字节(对齐时为 716 字节)。在这种情况下,行指针可节省18% 多一点。■

By far the most common use of the row-pointer layout in C is to represent arrays of strings. A typical example appears in Figure 8.9. In this example (representing the days of the week), the row-pointer memory layout consumes 57 bytes for the characters themselves (including a NUL byte at the end of each string), plus 28 bytes for pointers (assuming a 32-bit architecture), for a total of 85 bytes. The contiguous layout alternative devotes 10 bytes to each day (room enough for Wednesday and its NUL byte), for a total of 70 bytes. The additional space required for the row-pointer organization comes to 21 percent. In some cases, row pointers may actually save space. A Java compiler written in C, for example, would probably use row pointers to store the character-string representations of the 51 Java keywords and word-like literals. This data structure would use 55 × 4 = 220 bytes for the pointers (on a 32-bit machine), plus 366 bytes for the keywords, for a total of 586 bytes. Since the longest keyword (synchronized) requires 13 bytes (including space for the terminating NUL), a contiguous two-dimensional array would consume 55 × 13 = 715 bytes (716 when aligned). In this case, row pointers save a little over 18%. ■

编号:F08-09-9780124104099
图 8.9 C 语言中的连续数组分配与行指针。左边的声明是一个真正的二维数组。斜线框是NUL字节;阴影区域是孔。右边的声明是一个指向字符数组的参差不齐的指针数组。字符数组可以位于内存中的任何位置 - 彼此相邻或分开,并且顺序任意。在这两种情况下,我们都省略了声明中的边界,可以从初始化器(集合)的大小推断出来。这两种数据结构都允许使用双下标访问单个字符,但内存布局(和相应的地址算法)却大不相同。

设计与实现

Design & Implementation

8.4 数组布局

8.4 Array layout

内存中数组的布局,就像记录字段的顺序一样,与设计和实现中的权衡密切相关。虽然列主布局似乎在现代机器上没有任何优势,但它在 Fortran 中的持续使用意味着程序员必须了解底层实现,以便在嵌套循环中实现良好的局部性。同样,行指针布局在现代机器上没有性能优势(并且可能会降低性能,至少对于数字代码而言),但它更适合 Java 等语言的“引用对象”数据组织。它对空间消耗和局部性的影响可能是积极的,也可能是消极的,具体取决于各个应用程序的细节。

The layout of arrays in memory, like the ordering of record fields, is intimately tied to tradeoffs in design and implementation. While column-major layout appears to offer no advantages on modern machines, its continued use in Fortran means that programmers must be aware of the underlying implementation in order to achieve good locality in nested loops. Row-pointer layout, likewise, has no performance advantage on modern machines (and a likely performance penalty, at least for numeric code), but it is a more natural fit for the “reference to object” data organization of languages like Java. Its impacts on space consumption and locality maybe positive or negative, depending on the details of individual applications.

地址计算

Address Calculations

例 8.25

Example 8.25

索引连续数组

Indexing a contiguous array

对于通常的连续数组布局,计算特定元素的地址有些复杂,但很简单。假设编译器对三维数组给出以下声明:

For the usual contiguous layout of arrays, calculating the address of a particular element is somewhat complicated, but straightforward. Suppose a compiler is given the following declaration for a three-dimensional array:

A :数组 [ L 1 .. U 1 ] 的数组 [ L 2 .. U 2 ] 的数组 [ L 3 .. U 3 ] 的 elem.type;

A : array [L1 .. U1] of array [L2 .. U2] of array [L3 .. U3] of elem.type;

让我们定义三个维度的大小常量:

Let us define constants for the sizes of the three dimensions:

年代3=元素的大小_类型年代2=3大号3+1×年代3年代1=2大号2+1×年代2

S3=size of elem_typeS2=(U3L3+1)×S3S1=(U2L2+1)×S2

si1_e

这里,行 ( S 2 )的大小是单个元素 ( S 3 ) 的大小乘以行中元素的数量(假设为行主布局)。平面 ( S 1 ) 的大小是行 ( S 2 ) 的大小乘以平面中的行数。A [i, j, k]的地址为

Here the size of a row (S2) is the size of an individual element (S3) times the number of elements in a row (assuming row-major layout). The size of a plane (S1) is the size of a row (S2) times the number of rows in a plane. The address of A[i, j, k] is then

地址A+大号1×年代1+大号2×年代2+大号3×年代3

address of A+(iL1)×S1+(jL2)×S2+(kL3)×S3

si2_e

如上所述,此计算涉及三次乘法和六次加法/减法。我们可以在运行时计算整个表达式,但在大多数情况下,稍加重新排列就会发现,大部分计算可以在编译时执行。特别是,如果在编译时已知数组的边界,则S 1S 2S 3是编译时常量,并且下限的减法可以在括号外分布:

As written, this computation involves three multiplications and six additions/subtractions. We could compute the entire expression at run time, but in most cases a little rearrangement reveals that much of the computation can be performed at compile time. In particular, if the bounds of the array are known at compile time, then S1, S2, and S3 are compile-time constants, and the subtractions of lower bounds can be distributed out of the parentheses:

×年代1+×年代2+×年代3+地址A[大号1×年代1+大号2×年代2+大号3×年代3]

(i×S1)+(j×S2)+(k×S3)+address of A[(L1×S1)+(L2×S2)+(L3×S3)]

si3_e

该公式中的括号表达式是编译时常量(假设A的边界是静态已知的)。如果A是全局变量,则A的地址也是静态已知的,并且可以合并到括号表达式中。如果A是子程序的局部变量(具有静态形状),则A的地址可以在运行时分解为静态偏移量(包含在括号表达式中)加上框架指针的内容。我们可以将A的地址加上括号表达式视为计算一个虚数组的位置,该数组的第[i, j, k]个元素与A的元素重合,但其每个维度的下限为零。该虚数组如图8.10所示。■

The bracketed expression in this formula is a compile-time constant (assuming the bounds of A are statically known). If A is a global variable, then the address of A is statically known as well, and can be incorporated in the bracketed expression. If A is a local variable of a subroutine (with static shape), then the address of A can be decomposed into a static offset (included in the bracketed expression) plus the contents of the frame pointer at run time. We can think of the address of A plus the bracketed expression as calculating the location of an imaginary array whose [i, j, k]th element coincides with that of A, but whose lower bound in each dimension is zero. This imaginary array is illustrated in Figure 8.10. ■

传真:08-10-9780124104099
图 8.10 具有非零下限的数组的虚拟位置。通过在编译时计算数组索引的常量部分,我们可以有效地索引一个数组,该数组的起始地址在内存中偏移,但其下限全为零。

例 8.26

Example 8.26

数组索引的静态部分和动态部分

Static and dynamic portions of an array index

如果在编译时已知i、j和/或k ,则A[i, j, k]地址计算的其他部分将从动态部分移至静态部分上述公式。如果所有下标都已知,则可以静态计算整个地址。相反,如果在编译时不知道数组的任何边界,则计算的部分将从公式的静态部分移至动态部分。例如,如果L 1直到运行时才知道,但在编译时已知k3,则计算变为

If i, j, and/or k is known at compile time, then additional portions of the calculation of the address of A[i, j, k] will move from the dynamic to the static part of the formula shown above. If all of the subscripts are known, then the entire address can be calculated statically. Conversely, if any of the bounds of the array are not known at compile time, then portions of the calculation will move from the static to the dynamic part of the formula. For example, if L1 is not known until run time, but k is known to be 3 at compile time, then the calculation becomes

×年代1+×年代2大号1×年代1+地址A[大号2×年代2+大号3×年代33×年代3]

(i×S1)+(j×S2)(L1×S1)+address of A[(L2×S2)+(L3×S3)(3×S3)]

si4_e

再次强调,括号内的部分可以在编译时计算。如果下限始终限制为零(就像在 C 中一样),那么它们永远不会增加运行时成本。■

Again, the bracketed part can be computed at compile time. If lower bounds are always restricted to zero, as they are in C, then they never contribute to run-time cost. ■

在所有示例中,我们都忽略了对越界下标的动态语义检查问题。我们将在练习 8.10中探讨这些代码。在 C-17.5.2 节中,我们将考虑可用于静态消除许多检查的代码改进技术,特别是在枚举控制循环中。

In all our examples, we have ignored the issue of dynamic semantic checks for out-of-bound subscripts. We explore the code for these in Exercise 8.10. In Section C-17.5.2 we will consider code improvement techniques that can be used to eliminate many checks statically, particularly in enumeration-controlled loops.

例 8.27

Example 8.27

索引复杂结构

Indexing complex structures

地址计算的“静态部分”和“动态部分”的概念不仅仅适用于数组。例如,假设V是一个混乱的本地记录数组,其中包含字段M中的嵌套二维数组。V [i].M[3, j]的地址可以计算为

The notion of “static part” and “dynamic part” of an address computation generalizes to more than just arrays. Suppose, for example, that V is a messy local array of records containing a nested, two-dimensional array in field M. The address of V[i].M[3, j] could be calculated as

×年代1si5_e大号1×年代1si6_e
+ M的偏移量作为字段
+3大号1×年代1si7_e
+×年代2si8_e
大号2×年代2si9_e
+ fp
+框架中的V偏移

设计与实现

Design & Implementation

8.5 数组索引的下限

8.5 Lower bounds on array indices

在 C 语言中,每个数组维度的下限始终为零。通常认为语言设计者采用这种约定是为了避免在运行时从索引中减去下限,从而避免潜在的效率低下。但是,正如我们的讨论所示,编译器可以通过转换为虚拟起始位置来避免任何运行时成本。(当下限具有非常大的绝对值时,此语句的一个例外是:如果任何索引(按元素大小缩放)超过位移模式寻址可用的最大偏移量[在 RISC 计算机上通常为 2 15字节],则运行时可能仍需要减法。)

In C, the lower bound of every array dimension is always zero. It is often assumed that the language designers adopted this convention in order to avoid subtracting lower bounds from indices at run time, thereby avoiding a potential source of inefficiency. As our discussion has shown, however, the compiler can avoid any run-time cost by translating to a virtual starting location. (The one exception to this statement occurs when the lower bound has a very large absolute value: if any index (scaled by element size) exceeds the maximum offset available with displacement mode addressing [typically 215 bytes on RISC machines], then subtraction may still be required at run time.)

更可能的解释在于 C 语言中数组和指针的互操作性(第 8.5.1 节):C 语言的约定允许编译器生成指针索引操作的代码,而不必担心指针指向的数组的下限。有趣的是,Fortran 数组维度的默认下限为 1;除非程序员明确指定下限为 0,否则编译器必须始终转换为虚拟起始位置。

A more likely explanation lies in the interoperability of arrays and pointers in C (Section 8.5.1): C's conventions allow the compiler to generate code for an index operation on a pointer without worrying about the lower bound of the array into which the pointer points. Interestingly, Fortran array dimensions have a default lower bound of 1; unless the programmer explicitly specifies a lower bound of 0, the compiler must always translate to a virtual starting location.

这里左边的计算必须在运行时执行;而右边的计算可以在编译时执行。(边界和大小的表示法将变量的名称放在上标中,将维度放在下标中:是M的第二维的下限。)■大号2si10_e

Here the calculations on the left must be performed at run time; the calculations on the right can be performed at compile time. (The notation for bounds and size places the name of the variable in a superscript and the dimension in a subscript: L2M is the lower bound of the second dimension of M.) ■

例 8.28

Example 8.28

索引行指针数组

Indexing a row-pointer array

使用行指针的数组的地址计算相对简单。以我们的三维数组A为例,表达式A[i, j, k]在 C 表示法中等同于(*(*A[i])[j])[k]。如果中间指针加载在缓存中都命中,则评估此表达式的代码的成本可能与连续分配情况的成本相当(示例 8.26)。如果中间加载在缓存中未命中,则速度会大大降低。在 1970 年代的 CISC 机器上,平衡可能会向另一个方向倾斜:乘法会更慢,而内存访问会更快。无论如何(连续或行指针分配,旧机器或新机器),当多个数组引用使用相同的下标表达式或数组引用嵌入循环中时,通常可以实现重要的代码改进。■

Address calculation for arrays that use row pointers is comparatively straight-forward. Using our three-dimensional array A as an example, he expression A[i, j, k] is equivalent, in C notation, to (*(*A[i])[j])[k]. If the intermediate pointer loads both hit in the cache, the code to evaluate this expression is likely to be comparable in cost to that of the contiguous allocation case (Example 8.26). If the intermediate loads miss in the cache, it will be substantially slower. On a 1970s CISC machine, the balance would probably have tipped the other way: multiplies would have been slower, and memory accesses faster. In any event (contiguous or row-pointer allocation, old or new machine), important code improvements will often be possible when several array references use the same subscript expression, or when array references are embedded in loops. ■

08-01-9780124104099检查你的理解

Check Your Understanding

8. 什么是数组切片? 切片有什么用途?

8. What is an array slice? For what purposes are slices useful?

9. 二维数组和一维数组的数组有显著的区别吗?

9. Is there any significant difference between a two-dimensional array and an array of one-dimensional arrays?

10. 数组的形状是什么样的?

10. What is the shape of an array?

11. 什么是毒品载体?它起什么作用?

11. What is a dope vector? What purpose does it serve?

12. 在什么情况下,子程序中声明的数组可以分配在栈中?什么情况下必须分配在堆中?

12. Under what circumstances can an array declared within a subroutine be allocated in the stack? Under what circumstances must it be allocated in the heap?

13. 什么是一致数组?

13. What is a conformant array?

14.讨论 数组的连续行指针布局的比较优势。

14. Discuss the comparative advantages of contiguous and row-pointer layout for arrays.

15.解释连续分配数组的 行优先列优先布局之间的区别。为什么程序员需要知道哪种布局编译器使用什么?为什么大多数语言设计者认为行主布局更好?

15. Explain the difference between row-major and column-major layout for contiguously allocated arrays. Why does a programmer need to know which layout the compiler uses? Why do most language designers consider row-major layout to be better?

16. 计算数组元素地址的工作有多少可以在编译时完成?有多少必须在运行时完成?

16. How much of the work of computing the address of an element of an array can be performed at compile time? How much must be performed at run time?

8.3 字符串

8.3 Strings

在某些语言中,字符串仅仅是一个字符数组。在其他语言中,字符串具有特殊地位,其操作无法用于其他类型的数组。Perl、Python 和 Ruby 等脚本语言具有大量内置字符串运算符和函数,包括基于正则表达式的复杂模式匹配功能。某些专用语言(尤其是 Icon)提供更复杂的机制,包括通用生成器和回溯搜索。我们将在第 14.4.2 节中更详细地考虑脚本语言的字符串和模式匹配功能。Icon 已在第 C-6.5.4 节中讨论。在本节的剩余部分,我们将重点讨论字符串在更传统的语言中的作用。

In some languages, a string is simply an array of characters. In other languages, strings have special status, with operations that are not available for arrays of other sorts. Scripting languages like Perl, Python, and Ruby have extensive suites of built-in string operators and functions, including sophisticated pattern matching facilities based on regular expressions. Some special-purpose languages—Icon, in particular—provide even more sophisticated mechanisms, including general-purpose generators and backtracking search. We will consider the string and pattern-matching facilities of scripting languages in more detail in Section 14.4.2. Icon was discussed in Section C-6.5.4. In the remainder of the current section we focus on the role of strings in more traditional languages.

几乎所有编程语言都允许将文字字符串指定为字符序列,通常用单引号或双引号括起来。大多数语言区分文字字符(通常用单引号分隔)和文字字符串(通常用双引号分隔)。少数语言不做这样的区分,将字符定义为长度为 1 的字符串。大多数语言还提供转义序列,允许非打印字符和引号出现在文字字符串内。

Almost all programming languages allow literal strings to be specified as a sequence of characters, usually enclosed in single or double quote marks. Most languages distinguish between literal characters (often delimited with single quotes) and literal strings (often delimited with double quotes). A few languages make no such distinction, defining a character as simply a string of length one. Most languages also provide escape sequences that allow nonprinting characters and quote marks to appear inside literal strings.

例 8.29

Example 8.29

C 和 C++ 中的字符转义

Character escapes in C and C++

C 和 C++ 提供了非常丰富的转义序列。任意字符都可以用反斜杠后跟 (a) 1 到 3 个八进制(8 进制)数字、(b) x一个或多个十六进制(16 进制)数字、(c) u正好 4 个十六进制数字或 (d) U正好 8 个十六进制数字来表示。\U表示法用于捕获侧栏 7.3 中描述的四字节(32 位)Unicode 字符集。\u表示法用于基本多文种平面中的字符。许多最常见的控制字符也有单字符转义序列,其中许多也被其他语言采用。例如,\n是换行符;\t是制表符;\r是回车符;\\是反斜杠。C# 省略了 C 和 C++ 的八进制序列; Java 还省略了 32 位扩展序列。■

C and C++ provide a very rich set of escape sequences. An arbitrary character can be represented by a backslash followed by (a) 1 to 3 octal (base 8) digits, (b) an x and one or more hexadecimal (base-16) digits, (c) a u and exactly four hexadecimal digits, or (d) a U and exactly eight hexadecimal digits. The \U notation is meant to capture the four-byte (32-bit) Unicode character set described in Sidebar 7.3. The \u notation is for characters in the Basic Multilingual Plane. Many of the most common control characters also have single-character escape sequences, many of which have been adopted by other languages as well. For example, \n is a line feed; \t is a tab; \r is a carriage return; \\ is a backslash. C# omits the octal sequences of C and C++; Java also omits the 32-bit extended sequences. ■

为字符串提供的操作集与语言设计者设想的实现密切相关。一些通常不允许数组动态更改大小的语言为字符串提供了这种灵活性。理由有两方面。首先,对可变长度字符串的操作是大量计算机应用程序的基础,在某种意义上“值得”特殊对待。其次,字符串是一维的,具有单字节元素,并且从不包含对其他任何内容的引用,这使动态大小字符串比一般动态数组更容易实现。

The set of operations provided for strings is strongly tied to the implementation envisioned by the language designer(s). Several languages that do not in general allow arrays to change size dynamically do provide this flexibility for strings. The rationale is twofold. First, manipulation of variable-length strings is fundamental to a huge number of computer applications, and in some sense “deserves” special treatment. Second, the fact that strings are one-dimensional, have one-byte elements, and never contain references to anything else makes dynamic-size strings easier to implement than general dynamic arrays.

例 8.30

Example 8.30

C 语言中的char*赋值

char* assignment in C

某些语言要求字符串值变量的长度在编写时就进行绑定,这样就可以将变量实现为当前堆栈框架中的连续字符数组。Ada 支持一些字符串操作,包括按字典顺序排列的赋值和比较。而 C 语言只提供创建指向字符串文字的指针的功能。由于 C 语言将数组和指针统一起来,所以甚至不支持赋值。给定声明char *s,语句s = “abc”使s指向静态存储中的常量“abc”。如果将s声明为数组而不是指针(char s[4]),则该语句将触发编译器的错误消息。要在 C 语言中将一个数组赋值给另一个数组,程序必须单独复制字符。■

Some languages require that the length of a string-valued variable be bound no later than elaboration time, allowing the variable to be implemented as a contiguous array of characters in the current stack frame. Ada supports a few string operations, including assignment and comparison for lexicographic ordering. C, on the other hand, provides only the ability to create a pointer to a string literal. Because of C's unification of arrays and pointers, even assignment is not supported. Given the declaration char *s, the statement s = “abc” makes s point to the constant “abc” in static storage. If s is declared as an array, rather than a pointer (char s[4]), then the statement will trigger an error message from the compiler. To assign one array into another in C, the program must copy the characters individually. ■

其他语言允许字符串值变量的长度在其生命周期内发生变化,要求将变量实现为堆中的块或块链。ML 和 Lisp 将字符串作为内置类型提供。C++、Java 和 C# 将它们作为预定义的对象类提供,以正式的面向对象意义提供。在所有这些语言中,字符串变量都是对字符串的引用。为这样的变量分配新值会使其引用不同的对象 - 每个这样的对象都是不可变的。连接和其他字符串运算符会隐式创建新对象。不再可从任何变量访问的对象所使用的空间将被自动回收。

Other languages allow the length of a string-valued variable to change over its lifetime, requiring that the variable be implemented as a block or chain of blocks in the heap. ML and Lisp provide strings as a built-in type. C++, Java, and C# provide them as predefined classes of object, in the formal, object-oriented sense. In all these languages a string variable is a reference to a string. Assigning a new value to such a variable makes it refer to a different object—each such object is immutable. Concatenation and other string operators implicitly create new objects. The space used by objects that are no longer reachable from any variable is reclaimed automatically.

8.4 集合

8.4 Sets

例 8.31

Example 8.31

Pascal 中的集合类型

Set types in Pascal

编程语言集合是任意数量的通用类型的不同值的无序集合。集合由 Pascal 引入,并被许多后续语言支持。集合元素所来自的类型称为基类型宇宙类型。Pascal 集合仅限于离散基类型,并重载+、*以分别提供集合并集、交集和差集运算。预期的实现是一个特征数组— 位向量,其长度(以位为单位)是基类型的不同值的数量。位向量中第k个位置的 1 表示基类型的第 k个元素是集合的成员;零表示不是。在使用 ASCII 的语言中,一组字符将占用 128 位(16 个字节)。在大多数机器上,对位向量集合的操作可以使用快速逻辑指令。并集是按位;交集是按位;差集是按位,然后是按位。■

A programming language set is an unordered collection of an arbitrary number of distinct values of a common type. Sets were introduced by Pascal, and have been supported by many subsequent languages. The type from which elements of a set are drawn is known as the base or universe type. Pascal sets were restricted to discrete base types, and overloaded +, *, and to provide set union, intersection, and difference operations, respectively. The intended implementation was a characteristic array—a bit vector whose length (in bits) is the number of distinct values of the base type. A one in the kth position in the bit vector indicates that the kth element of the base type is a member of the set; a zero indicates that it is not. In a language that uses ASCII, a set of characters would occupy 128 bits—16 bytes. Operations on bit-vector sets can make use of fast logical instructions on most machines. Union is bit-wise or; intersection is bit-wise and; difference is bit-wise not, followed bybit-wise and. ■

不幸的是,位向量对于大型基类型不太适用:一组整数(表示为位向量)在 32 位机器上会占用大约 500 兆字节。对于 64 位整数,位向量集占用的内存比目前世界上所有计算机所包含的内存还要多。由于这个问题,某些语言(包括早期版本的 Pascal,但不是 ISO 标准)将集合限制为少于某个固定值数量的基类型。

Unfortunately, bit vectors do not work well for large base types: a set of integers, represented as a bit vector, would consume some 500 megabytes on a 32-bit machine. With 64-bit integers, a bit-vector set would consume more memory than is currently contained on all the computers in the world. Because of this problem, some languages (including early versions of Pascal, though not the ISO standard) limited sets to base types of fewer than some fixed number of values.

对于从大集合中抽取的元素集合,大多数现代语言使用替代实现,其大小与现有元素的数量成正比,而不是与基类型中的值的数量成正比。大多数语言还提供了内置迭代器(第 6.5.3 节)来生成集合的元素。通常会区分有序列表和无序列表,有序列表的基类型必须支持某种排序概念,其迭代器按从小到大的顺序生成元素,无序列表的迭代器按任意顺序生成元素。有序集通常用跳跃列表或各种树来实现。无序列表通常用哈希表来实现。

For sets of elements drawn from a large universe, most modern languages use alternative implementations, whose size is proportional to the number of elements present, rather than to the number of values in the base type. Most languages also provide a built-in iterator (Section 6.5.3) to yield the elements of the set. A distinction is often made between sorted lists, whose base type must support some notion of ordering, and whose iterators yield the elements smallest-to-largest, and unordered lists, whose iterators yield the elements in arbitrary order. Ordered sets are commonly implemented with skip lists or various sorts of trees. Unordered sets are commonly implemented with hash tables.

例 8.32

Example 8.32

在 Go 中使用 map 模拟集合

Emulating a set with a map in Go

一些语言(例如 Python 和 Swift)将集合作为内置类型构造函数提供。Python 版本可在示例 14.67中看到。在许多面向对象语言中,集合由标准库支持。一些语言和库没有内置集合构造函数,但提供关联数组(也称为“哈希”、“字典”或“映射”)。这些可用于模拟无序集,通过将所有(且仅)所需元素映射到某个虚拟值。例如,在 Go 中,我们可以写

Some languages (Python and Swift, for example) provide sets as a built-in type constructor. The Python version can be seen in Example 14.67. In many object-oriented languages, sets are supported by the standard library instead. A few languages and libraries have no built-in set constructor, but do provide associative arrays (also known as “hashes,” “dictionaries,” or “maps”). These can be used to emulate unordered sets, by mapping all (and only) the desired elements to some dummy value. In Go, for example, we can write

my_set := make(map[int]bool)// 从 int 映射到 bool
my_set[3] = true// 在映射中插入 <3, true>
删除(my_set,i)// 如果存在,则删除 <i, true>
如果 my_set[j] { …// 如果存在则为真

如果M是Go 中从类型D到类型R 的映射,并且如果kD未映射到R中的任何内容,则表达式M[k]将返回类型R的“零值” 。对于布尔值,零值恰好是false ,因此如果j不在my_set中,则示例最后一行中的测试将返回false。删除不再存在的元素比将其明确映射到false更可取,因为删除会回收底层哈希表中的空间;映射到false则不会。■

If M is a mapping from type D to type R in Go, and if kD is not mapped to anything in R, the expression M[k] will return the “zero value” of type R. For Booleans, the zero value happens to be false, so the test in the last line of our example will return false if j is not in my_set. Deleting a no-longer-present element is preferable to mapping it explicitly to false, because deletion reclaims the space in the underlying hash table; mapping to false does not. ■

8.5 指针和递归类型

8.5 Pointers and Recursive Types

递归类型是指其对象可能包含对该类型的其他对象的一个​​或多个引用的类型。大多数递归类型都是记录,因为它们需要包含除引用之外的其他内容,这意味着存在异构字段。递归类型用于构建各种“链接”数据结构,包括列表和树。

A recursive type is one whose objects may contain one or more references to other objects of the type. Most recursive types are records, since they need to contain something in addition to the reference, implying the existence of heterogeneous fields. Recursive types are used to build a wide variety of “linked” data structures, including lists and trees.

在使用变量引用模型的语言中,类型为foo的记录很容易包含对类型为foo的另一个记录的引用:每个变量(因此每个记录字段)都是引用。在使用变量值模型的语言中,递归类型需要指针的概念一个变量(或字段),其值是对某个对象的引用。指针最早是在 PL/I 中引入的。

In languages that use a reference model of variables, it is easy for a record of type foo to include a reference to another record of type foo: every variable (and hence every record field) is a reference anyway. In languages that use a value model of variables, recursive types require the notion of a pointer: a variable (or field) whose value is a reference to some object. Pointers were first introduced in PL/I.

在某些语言(例如 Pascal、Modula-3 和 Ada 83)中,指针被限制为仅指向堆中的对象。创建新指针值的唯一方法(不使用变体记录或强制类型转换来绕过类型系统)是调用内置函数,该函数在堆中分配一个新对象并返回指向该对象的指针。在其他语言(无论是新语言还是旧语言)中,都可以使用“地址”运算符创建指向非堆对象的指针。我们将在下面的第一小节中更详细地研究指针操作以及引用和值模型的影响。

In some languages (e.g., Pascal, Modula-3, and Ada 83), pointers were restricted to point only to objects in the heap. The only way to create a new pointer value (without using variant records or casts to bypass the type system) was to call a built-in function that allocated a new object in the heap and returned a pointer to it. In other languages, both old and new, one can create a pointer to a nonheap object by using an “address of” operator. We will examine pointer operations and the ramifications of the reference and value models in more detail in the first subsection below.

在任何允许从堆中分配新对象的语言中,都会出现一个问题:如何以及何时回收不再需要的对象的存储空间?在短期程序中,简单地让存储空间闲置是可以接受的,但在大多数情况下,必须回收未使用的空间,以便为其他内容腾出空间。无法回收不再需要的对象空间的程序被称为“内存泄漏”。如果这样的程序运行时间过长,可能会耗尽空间并崩溃。

In any language that permits new objects to be allocated from the heap, the question arises: how and when is storage reclaimed for objects that are no longer needed? In short-lived programs it may be acceptable simply to leave the storage unused, but in most cases unused space must be reclaimed, to make room for other things. A program that fails to reclaim the space for objects that are no longer needed is said to “leak memory.” If such a program runs for an extended period of time, it may run out of space and crash.

某些语言(包括 C、C++ 和 Rust)要求程序员明确回收空间。其他语言(包括 Java、C#、Scala、Go 以及所有函数式和脚本语言)要求语言实现自动回收未使用的对象。显式存储回收简化了语言实现,但增加了程序员忘记回收不再活动的对象(从而泄漏内存)或意外回收仍在使用的对象(从而创建悬空引用)的可能性。自动存储回收(也称为垃圾收集)大大简化了程序员的任务,但会带来一定的运行时成本,并且引发了语言实现如何区分垃圾和活动对象的问题。我们将分别在第 8.5.2 节第 8.5.3节中进一步讨论悬空引用和垃圾收集。

Some languages, including C, C++, and Rust, require the programmer to reclaim space explicitly. Other languages, including Java, C#, Scala, Go, and all the functional and scripting languages, require the language implementation to reclaim unused objects automatically. Explicit storage reclamation simplifies the language implementation, but raises the possibility that the programmer will forget to reclaim objects that are no longer live (thereby leaking memory), or will accidentally reclaim objects that are still in use (thereby creating dangling references). Automatic storage reclamation (otherwise known as garbage collection) dramatically simplifies the programmer's task, but imposes certain runtime costs, and raises the question of how the language implementation is to distinguish garbage from active objects. We will discuss dangling references and garbage collection further in Sections 8.5.2 and 8.5.3, respectively.

8.5.1 语法和操作

8.5.1 Syntax and Operations

指针上的操作包括堆中对象的分配和释放、指针的取消引用以访问其指向的对象以及赋值从一个指针到另一个指针的转换。这些操作的行为很大程度上取决于语言是函数式的还是命令式的,以及它对变量/名称采用的是引用模型还是值模型。

Operations on pointers include allocation and deallocation ofobjects in the heap, dereferencing of pointers to access the objects to which they point, and assignment of one pointer into another. The behavior of these operations depends heavily on whether the language is functional or imperative, and on whether it employs a reference or value model for variables/names.

设计与实现

Design & Implementation

8.6 指针的实现

8.6 Implementation of pointers

程序员(甚至是教科书作者)常常将指针等同于地址,但这是错误的。指针是一个高级概念:对对象的引用。地址是一个低级概念:内存中字的位置。指针通常实现为地址,但并非总是如此。在具有分段内存架构的机器上,指针可能由段 ID 和段内的偏移量组成。在试图捕获悬垂引用使用的语言中,指针可能同时包含地址和访问键。

It is common for programmers (and even textbook writers) to equate pointers with addresses, but this is a mistake. A pointer is a high-level concept: a reference to an object. An address is a low-level concept: the location of a word in memory. Pointers are often implemented as addresses, but not always. On a machine with a segmented memory architecture, a pointer may consist of a segment id and an offset within the segment. In a language that attempts to catch uses of dangling references, a pointer may contain both an address and an access key.

函数式语言通常采用引用模型来命名(纯函数式语言没有变量或赋值)。函数式语言中的对象往往根据需要自动分配,其结构由语言实现决定。命令式语言中的变量可以使用值模型或引用模型,或者两者的组合。在使用值模型的 C 或 Ada 中,赋值A = B会将B的值放入A中。如果我们想要B引用一个对象,并且想要A = BA引用B引用的对象,那么AB必须是指针。在使用引用模型的 Smalltalk 或 Ruby 中,赋值A = B总是让A引用B引用的同一个对象。

Functional languages generally employ a reference model for names (a purely functional language has no variables or assignments). Objects in a functional language tend to be allocated automatically as needed, with a structure determined by the language implementation. Variables in an imperative language may use either a value or a reference model, or some combination of the two. In C or Ada, which employ a value model, the assignment A = B puts the value of B into A. If we want B to refer to an object, and we want A = B to make A refer to the object to which B refers, then A and B must be pointers. In Smalltalk or Ruby, which employ a reference model, the assignment A = B always makes A refer to the same object to which B refers.

Java 走的是中间路线,其中引用模型的通常实现在语言语义中明确说明。内置 Java 类型(整数、浮点数、字符和布尔值)的变量采用值模型;用户定义类型(字符串、数组和面向对象意义上的其他对象)的变量采用引用模型。如果 A 和 B 是内置类型,则 Java 中的赋值A = B会将B的值放入A ;如果AB是用户定义类型,则它使A引用B所引用的对象。默认情况下,C# 镜像 Java,但其他语言功能(明确标记为“不安全)允许系统程序员在需要时使用指针。

Java charts an intermediate course, in which the usual implementation of the reference model is made explicit in the language semantics. Variables of built-in Java types (integers, floating-point numbers, characters, and Booleans) employ a value model; variables of user-defined types (strings, arrays, and other objects in the object-oriented sense of the word) employ a reference model. The assignment A = B in Java places the value of B into A if A and B are of built-in type; it makes A refer to the object to which B refers if A and B are of user-defined type. C# mirrors Java by default, but additional language features, explicitly labeled “unsafe,” allow systems programmers to use pointers when desired.

参考模型

Reference Model

例 8.33

Example 8.33

OCaml 中的树类型

Tree type in OCaml

在 ML 系列语言中,变体机制可用于声明递归类型(这里以 OCaml 语法显示):

In ML-family languages, the variant mechanism can be used to declare recursive types (shown here in OCaml syntax):

类型 chr_tree = 空 | char * chr_tree * chr_tree 的节点;;

type chr_tree = Empty | Node of char * chr_tree * chr_tree;;

这里的chr_tree要么是叶子,要么是由一个字符和两个子树组成的节点。(更多详细信息请参见第 11.4.3 节。)

Here a chr_tree is either an Empty leaf or a Node consisting of a character and two child trees. (Further details can be found in Section 11.4.3.)

在 OCaml 中,将chr_tree包含在chr_tree中是很自然的,因为每个变量都是一个引用。树Node ('R', Node ('X', Empty, Empty), Node ('Y', Node ('Z', Empty, Empty), Node ('W', Empty, Empty)))在内存中的表示方式很可能如图8.11所示。该图右侧的每个矩形表示从堆分配的存储块。实际上,树是一个标记为指示它是Node的元组(记录) 。此元组又引用另外两个也被标记为Nodes的元组。在树的边缘是标记为Empty的元组;这些元组不包含进一步的引用。因为所有Empty 元组都是相同的,所以实现可以只使用一个,并让每个引用都指向它。■

It is natural in OCaml to include a chr_tree within a chr_tree because every variable is a reference. The tree Node ('R', Node ('X', Empty, Empty), Node ('Y', Node ('Z', Empty, Empty), Node ('W', Empty, Empty))) would most likely be represented in memory as shown in Figure 8.11. Each individual rectangle in the right-hand portion of this figure represents a block of storage allocated from the heap. In effect, the tree is a tuple (record) tagged to indicate that it is a Node. This tuple in turn refers to two other tuples that are also tagged as Nodes. At the fringe of the tree are tuples that are tagged as Empty; these contain no further references. Because all Empty tuples are the same, the implementation is free to use just one, and to have every reference point to it. ■

传真:08-11-9780124104099
图 8.11 ML 中树的实现。左下角显示了抽象(概念)树。

例 8.34

Example 8.34

Lisp 中的树类型

Tree type in Lisp

在 Lisp 中,它使用变量的引用模型,但不是静态类型,我们的树可以用文本形式指定为' (#\R (#\X ()()) (#\Y (#\Z ()()) (#\W ()())))。每一级括号都将列表的元素括起来。在本例中,最外层的列表包含三个元素:字符R和嵌套的列表来表示左子树和右子树。 (前缀#\符号与其他语言中括住引号的用途相同。)从语义上讲,每个列表都是一对引用:一个指向头部,一个指向列表的其余部分。 如我们在第 8.5.1 节中所述,这些语义几乎总是反映在包含两个指针的cons单元的实现中。 因此,二叉树可以表示为三元素(三个cons单元)列表,如图8.12所示。 在图的顶层,第一个cons单元指向R;第二个和第三个指向表示左子树和右子树的嵌套列表。 每个内存块都被标记以指示它是cons单元还是原子。原子是除cons单元之外的任何东西; 即内置类型(整数、实数、字符、字符串等)的对象,或用户定义的结构(记录)或数组。 Lisp 列表的一致性(一切都是cons单元或原子)使得编写多态函数变得容易,尽管没有 ML 的静态类型检查。■

In Lisp, which uses a reference model of variables but is not statically typed, our tree could be specified textually as ' (#\R (#\X ()()) (#\Y (#\Z ()()) (#\W ()()))). Each level of parentheses brackets the elements of a list. In this case, the outermost such list contains three elements: the character R and nested lists to represent the left and right subtrees. (The prefix #\ notation serves the same purpose as surrounding quotes in other languages.) Semantically, each list is a pair of references: one to the head and one to the remainder of the list. As we noted in Section 8.5.1, these semantics are almost always reflected in the implementation by a cons cell containing two pointers. A binary tree can thus be represented as a three-element (three cons cell) list, as shown in Figure 8.12. At the top level of the figure, the first cons cell points to R; the second and third point to nested lists representing the left and right subtrees. Each block of memory is tagged to indicate whether it is a cons cell or an atom. An atom is anything other than a cons cell; that is, an object of a built-in type (integer, real, character, string, etc.), or a user-defined structure (record) or array. The uniformity of Lisp lists (everything is a cons cell or an atom) makes it easy to write polymorphic functions, though without the static type checking of ML. ■

传真:08-12-9780124104099
图 8.12 Lisp 中树的实现。斜线表示空指针。CA标签用于区分两种内存块:cons 单元和包含原子的块。

如果在 ML 或 Lisp 中以纯函数式风格编程,则使用递归类型创建的数据结构将变成非循环的。新对象引用旧对象,但旧对象永远不会改变,因此永远不会指向新对象。循环结构通常使用语言的命令式特征来定义。(有关此规则的例外情况,请参见练习 8.21。)在 ML 中,命令式特征包括显式指针概念,下面在“值模型”中简要讨论。

If one programs in a purely functional style in ML or in Lisp, the data structures created with recursive types turn out to be acyclic. New objects refer to old ones, but old ones never change, and thus never point to new ones. Circular structures are typically defined by using the imperative features of the languages. (For an exception to this rule, see Exercise 8.21.) In ML, the imperative features include an explicit notion of pointer, discussed briefly under “Value Model” below.

例 8.35

Example 8.35

OCaml 中的相互递归类型

Mutually recursive types in OCaml

即使在以函数式风格编写时,也经常会发现需要相互递归类型。例如,在编译器中,符号表记录和语法树节点可能需要相互引用。表示子例程调用的语法树节点需要引用表示子例程的符号表记录。符号表记录则需要引用表示子例程代码的子树根部的语法树节点。如果一次声明一个类型,并且必须先声明名称才能使用,那么首先声明的相互递归类型将是无法引用其他类型。ML 系列语言通过允许将类型声明为一组来解决此问题。使用 OCaml 语法,

Even when writing in a functional style, one often finds a need for types that are mutually recursive. In a compiler, for example, it is likely that symbol table records and syntax tree nodes will need to refer to each other. A syntax tree node that represents a subroutine call will need to refer to the symbol table record that represents the subroutine. The symbol table record, for its part, will need to refer to the syntax tree node at the root of the subtree that represents the subroutine's code. If types are declared one at a time, and if names must be declared before they can be used, then whichever mutually recursive type is declared first will be unable to refer to the other. ML family languages address this problem by allowing types to be declared together as a group. Using OCaml syntax,

类型 subroutine_info = {code: syn_tree_node; …} (* 记录 *)

type subroutine_info = {code: syn_tree_node; …} (* record *)

 并且 subr_call_info = {subr: sym_tab_rec; …} (* 记录 *)

 and subr_call_info = {subr: sym_tab_rec; …} (* record *)

 和 sym_tab_rec = (* 变体 *)

 and sym_tab_rec =       (* variant *)

  …的变量

  Variable of …

 | 类型 …

 | Type of …

 | …

 | …

 | subroutine_info 的子程序

 | Subroutine of subroutine_info

并且 syn_tree_node = (* 变体 *)

and syn_tree_node =       (* variant *)

  表达…

  Expression of …

 | 循环…

 | Loop of …

 | …

 | …

 | subr_call 的 subr_call_info;;

 | Subr_call of subr_call_info;;

这种相互递归类型在 Lisp 中很简单,因为它是动态类型的。(Common Lisp 包含结构概念,但未声明字段类型。在更简单的 Lisp 方言中,程序员使用嵌套列表,其中字段仅仅是位置约定。)■

Mutually recursive types of this sort are trivial in Lisp, since it is dynamically typed. (Common Lisp includes a notion of structures, but field types are not declared. In simpler Lisp dialects programmers use nested lists in which fields are merely positional conventions.) ■

价值模型

Value Model

例 8.36

Example 8.36

Ada 和 C 中的树类型

Tree types in Ada and C

在 Ada 中,我们的树数据类型声明如下:

In Ada, our tree data type would be declared as follows:

类型 chr_tree;

type chr_tree;

类型 chr_tree_ptr 是访问 chr_tree;

type chr_tree_ptr is access chr_tree;

类型 chr_tree 是记录

type chr_tree is record

 左,右:chr_tree_ptr;

 left, right : chr_tree_ptr;

 val:字符;

 val : character;

结束记录;

end record;

在 C 语言中,等效声明为

In C, the equivalent declaration is

结构 chr_tree {

struct chr_tree {

 结构 chr_tree *left,*right;

 struct chr_tree *left, *right;

 烧焦瓦尔;

 char val;

};

};

如3.3.3节所述,Ada 和C 都依赖不完全类型声明来适应递归定义。■

As mentioned in Section 3.3.3, Ada and C both rely on incomplete type declarations to accommodate recursive definition. ■

例 8.37

Example 8.37

分配堆节点

Allocating heap nodes

Ada 或 C 中没有用于链接数据结构的聚合语法;必须逐个节点构建树。要从堆中分配新节点,程序员需要调用内置函数。在 Ada 中:

No aggregate syntax is available for linked data structures in Ada or C; a tree must be constructed node by node. To allocate a new node from the heap, the programmer calls a built-in function. In Ada:

my_ptr:=新的chr_tree;

my_ptr := new chr_tree;

在 C 中:

In C:

my_ptr = malloc(sizeof(struct chr_tree));

my_ptr = malloc(sizeof(struct chr_tree));

C 的malloc被定义为库函数,而不是语言的内置部分(尽管许多编译器将其识别为特例并进行优化)。程序员必须明确指定分配对象的大小,虽然返回值(类型为void*)可以赋给任何指针,但赋值不是类型安全的。■

C's malloc is defined as a library function, not a built-in part of the language (though many compilers recognize and optimize it as a special case). The programmer must specify the size of the allocated object explicitly, and while the return value (of type void*) can be assigned into any pointer, the assignment is not type safe. ■

例 8.38

Example 8.38

面向对象堆节点分配

Object-oriented allocation of heap nodes

C++、Java 和 C#使用内置的、类型安全的new替换malloc

C++, Java, and C# replace malloc with a built-in, type-safe new:

my_ptr = 新的 chr_tree( arg_list );

my_ptr = new chr_tree(arg_list);

除了“知道”请求类型的大小之外,C++/Java/C# new还将自动调用任何用户指定的构造函数(初始化)函数,并传递指定的参数列表。类似但不太灵活的是,Ada 的new可以为分配的对象指定初始值:

In addition to “knowing” the size of the requested type, the C++/Java/C# new will automatically call any user-specified constructor (initialization) function, passing the specified argument list. In a similar but less flexible vein, Ada's new may specify an initial value for the allocated object:

my_ptr := new chr_tree'(null, null, 'X');

my_ptr := new chr_tree'(null, null, 'X');

例 8.39

Example 8.39

基于指针的树

Pointer-based tree

在 C 或 Ada 中分配并链接适当的节点后,我们的树示例可能如图8.13所示实现。叶子节点与内部节点的区别在于,叶子节点的两个指针字段为空。■

After we have allocated and linked together appropriate nodes in C or Ada, our tree example is likely to be implemented as shown in Figure 8.13. A leaf is distinguished from an internal node simply by the fact that its two pointer fields are null. ■

传真:08-13-9780124104099
图 8.13 具有显式指针的语言中树的典型实现。如图8.12所示,穿过方框的斜线表示空指针

例 8.40

Example 8.40

指针取消引用

Pointer dereferencing

为了访问指针所引用的对象,大多数语言都使用显式取消引用运算符。在 Pascal 和 Modula 中,此运算符采用后缀“向上箭头”的形式:

To access the object referred to by a pointer, most languages use an explicit dereferencing operator. In Pascal and Modula this operator took the form of a postfix “up-arrow”:

my_ptr^.val := 'X';

my_ptr^.val := 'X';

在 C 语言中它是一个前缀星号:

In C it is a prefix star:

(*my_ptr).val ='X';

(*my_ptr).val = 'X';

因为指针经常引用记录(结构),而前缀表示法不方便,所以 C 还提供了一个后缀“右箭头”运算符,它充当 Pascal 中“上箭头点”组合的作用:

Because pointers so often referto records (structs), forwhich the prefixnotation is awkward, C also provides a postfix “right-arrow” operator that plays the role of the “up-arrow dot” combination in Pascal:

my_ptr->val = 'X';

my_ptr->val = 'X';

例 8.41

Example 8.41

Ada 中的隐式解除引用

Implicit dereferencing in Ada

假设指针几乎总是指向记录,Ada 完全放弃了取消引用。可以使用相同的基于点的语法来访问记录foo的字段或foo指向的记录的字段,具体取决于foo的类型:

On the assumption that pointers almost always refer to records, Ada dispenses with dereferencing altogether. The same dot-based syntax can be used to access either a field of the record foo or a field of the record pointed to by foo, depending on the type of foo:

T :chr_tree;

T : chr_tree;

P :chr_tree_ptr;

P : chr_tree_ptr;

T.val := 'X';

T.val := 'X';

P.val :='Y';

P.val := 'Y';

在人们实际上想要命名指针所指向的整个对象的情况下,Ada 提供了一个特殊的“伪字段”,称为all

In those cases in which one actually wants to name the entire object referred to by a pointer, Ada provides a special “pseudofield” called all:

T:=P.全部;

T := P.all;

实质上,Ada 中的指针在需要时会自动取消引用。■

In essence, pointers in Ada are automatically dereferenced when needed. ■

例 8.42

Example 8.42

OCaml 中的指针取消引用

Pointer dereferencing in OCaml

OCaml 和其他 ML 方言的命令式特性包括赋值语句,但此语句要求左侧为指针:其效果是使指针引用右侧的对象。要访问指针引用的对象,可以使用感叹号作为前缀解引用运算符:

The imperative features of OCaml and other ML dialects include an assignment statement, but this statement requires that the left-hand side be a pointer: its effect is to make the pointer refer to the object on the right-hand side. To access the object referred to by a pointer, one uses an exclamation point as a prefix dereferencing operator:

let p = ref 2;; (* p 是指向 2 的指针 *)

let p = ref 2;; (* p is a pointer to 2 *)

p := 3;; (* p 现在指向 3 *)

p := 3;; (* p now points to 3 *)

让 n = !p 在…

let n = !p in …

 (* n 仅为 3 *)

 (* n is simply 3 *)

最终结果是使左值和右值之间的区别变得非常明显。大多数语言通过在每个赋值语句的右侧隐式取消引用变量来模糊区别。Ada 和 Go 通过在某些情况下自动取消引用指针来进一步模糊区别。■

The net result is to make the distinction between l-values and r-values very explicit. Most languages blur the distinction by implicitly dereferencing variables on the right-hand side of every assignment statement. Ada and Go blur the distinction further by dereferencing pointers automatically in certain circumstances. ■

例 8.43

Example 8.43

Lisp 中的赋值

Assignment in Lisp

Lisp 的命令式特性不包括解引用运算符。由于每个对象都有不言而喻的类型,并且赋值是使用一小组内置运算符执行的,因此永远不会对意图产生任何歧义。Common Lisp 中的赋值使用 setf 运算符 Scheme 使用set!、set-car!set-cdr!),而不是更常见的=:=。例如,如果foo引用一个列表,则(cdr foo)是列表中第一个节点的右侧(“列表的其余部分”)指针,赋值(set-cdr! foo foo)使该指针引用回foo,从而创建一个单节点循环列表:

The imperative features of Lisp do not include a dereferencing operator. Since every object has a self-evident type, and assignment is performed using a small set of built-in operators, there is never any ambiguity as to what is intended. Assignment in Common Lisp employs the setf operator (Scheme uses set!, set-car!, and set-cdr!), rather than the more common = or :=. For example, if foo refers to a list, then (cdr foo) is the right-hand (“rest of list”) pointer of the first node in the list, and the assignment (set-cdr! foo foo) makes this pointer refer back to foo, creating a one-node circular list:

u08-01-9780124104099

C 中的指针和数组

Pointers and Arrays in C

例 8.44

Example 8.44

C 中的数组名称和指针

Array names and pointers in C

在 C 语言中,指针和数组紧密相关。请考虑以下声明:

Pointers and arrays are closely linked in C. Consider the following declarations:

int n;
int *一个;/* 指向整数的指针 */
int b[10];/* 10 个整数的数组 */

现在以下所有内容均有效:

Now all of the following are valid:

1.a=b;/* 指向 b 的初始元素 */
2.n = a[3];
3.n=*(a+3);/* 相当于上一行 */
4.n = b[3];
5.n = *(b + 3);/* 相当于上一行 */

在大多数情况下,C 语言中无下标的数组名会自动转换为指向数组第一个元素(索引为零的元素)的指针,如下面第 1 行所示。(第 5 行体现了相同的转换。)第 3 行和第 5 行说明了指针算法:给定一个指向数组元素的指针,加上一个整数k会产生一个指向数组中后面k 个位置元素的指针(如果k是整数,则为前面的元素)。负数)。前缀*是指针取消引用运算符。指针运算仅在单个数组的范围内有效,但 C 编译器不需要检查这一点。

In most contexts, an unsubscripted array name in C is automatically converted to a pointer to the array's first element (the one with index zero), as shown here in line 1. (Line 5 embodies the same conversion.) Lines 3 and 5 illustrate pointer arithmetic: Given a pointer to an element of an array, the addition of an integer k produces a pointer to the element k positions later in the array (earlier if k is negative). The prefix * is a pointer dereference operator. Pointer arithmetic is valid only within the bounds of a single array, but C compilers are not required to check this.

值得注意的是,C 中的下标运算符[]实际上是根据指针算法定义的:第 2 行和第 4 行分别是第 3 行和第 5 行的语法糖。更准确地说,对于任何表达式E1E2 , E1[E2]定义为(*((E1) + (E2))),这当然与(*((E2) + (E1)))相同。(如果E1E2是复杂表达式,则在此定义中使用了额外的括号以避免任何优先级问题。)正确性仅要求[]的一个操作数具有数组或指针类型,另一个操作数具有整数类型。因此A[3]等同于3[A],这对大多数程序员来说是一个惊喜。■

Remarkably, the subscript operator [] in C is actually defined in terms of pointer arithmetic: lines 2 and 4 are syntactic sugar for lines 3 and 5, respectively. More precisely, E1[E2], for any expressions E1 and E2, is defined to be (*((E1) + (E2))), which is of course the same as (*((E2) + (E1))). (Extra parentheses have been used in this definition to avoid any questions of precedence if E1 and E2 are complicated expressions.) Correctness requires only that one operand of [] have an array or pointer type and the other have an integral type. Thus A[3] is equivalent to 3[A], something that comes as a surprise to most programmers. ■

设计与实现

Design & Implementation

8.7 堆栈粉碎

8.7 Stack smashing

数组下标和指针运算缺乏边界检查是 C 语言中错误和安全问题的主要来源。许多最臭名昭著的互联网病毒都是通过堆栈破坏(一种特别恶劣的缓冲区溢出攻击)传播的。考虑一个(非常简单的)例程,该例程旨在从输入流中读取数字:

The lack of bounds checking on array subscripts and pointer arithmetic is a major source of bugs and security problems in C. Many of the most infamous Internet viruses have propagated by means of stack smashing, a particularly nasty form of buffer overflow attack. Consider a (very naive) routine designed to read a number from an input stream:

int get_acct_num(文件 *s){

int get_acct_num(FILE *s) {

 char buf[100];

 char buf[100];

 字符 *p = 缓冲区;

 char *p = buf;

 做 {

 do {

  /* 从流 s 读取: */

  /* read from stream s: */

  *p = getc(s);

  *p = getc(s);

 /// 复制代码

 } while (*p++ != '\n');

 *p = '\0';

 *p = '\0';

 /* 将 ascii 转换为 int: */

 /* convert ascii to int: */

 返回 atoi(buf);

 return atoi(buf);

}

}

u08-02-9780124104099

如果流提供超过 100 个字符而没有换行符('\n') ,这些字符将覆盖buf范围之外的内存,如图中大白色箭头所示。细心的攻击者可能能够发明一个字符串,其位既包括有效机器指令序列,又包括子例程返回地址的替换值。当例程尝试返回时,它将跳转到攻击者的指令。

If the stream provides more than 100 characters without a newline ('\n'), those characters will overwrite memory beyond the confines of buf, as shown by the large white arrow in the figure. A careful attacker may be able to invent a string whose bits include both a sequence of valid machine instructions and a replacement value for the subroutine's return address. When the routine attempts to return, it will jump into the attacker's instructions instead.

可以通过手动检查 C 中的数组边界或配置硬件来阻止堆栈破坏,从而防止堆栈破坏(参见边栏 C-9.10)。但是,如果 C 语言设计为自动边界检查,那么这根本不会成为问题。

Stack smashing can be prevented by manually checking array bounds in C, or by configuring the hardware to prevent the execution of instructions in the stack (see Sidebar C-9.10). It would never have been a problem in the first place, however, if C had been designed for automatic bounds checks.

例 8.45

Example 8.45

C 语言中的指针比较和减法

Pointer comparison and subtraction in C

除了允许将整数添加到指针之外,C 还允许将指针彼此相减或进行比较以进行排序,前提是它们引用同一个数组的元素。例如,比较p < q测试p是否引用比q引用的元素更靠近数组开头的元素。表达式p - q返回pq引用的元素之间的数组位置数。所有对指针的算术运算都会根据引用对象的大小适当“缩放”其结果。对于具有行指针布局的多维数组,a[i][j]等同于(*(a+i))[j] 或*(a[i]+j) 或*(*(a+i)+j)。■

In addition to allowing an integer to be added to a pointer, C allows pointers to be subtracted from one another or compared for ordering, provided that they refer to elements of the same array. The comparison p < q, for example, tests to see if p refers to an element closer to the beginning of the array than the one referred to by q. The expression p - q returns the number of array positions that separate the elements to which p and q refer. All arithmetic operations on pointers “scale” their results as appropriate, based on the size of the referenced objects. For multidimensional arrays with row-pointer layout, a[i][j] is equivalent to (*(a+i))[j] or*(a[i]+j) or*(*(a+i)+j). ■

例 8.46

Example 8.46

C 中的指针和数组声明

Pointer and array declarations in C

尽管在 C 语言中指针和数组具有互操作性,但程序员需要知道这两者并不相同,特别是在变量声明的上下文中,声明时需要分配空间。指针变量的声明会分配空间来保存指针,而数组变量的声明会分配空间来保存整个数组。对于数组,声明必须指定每个维度的大小。因此,在声明时, int *a[n]将为n行指针分配空间;int a[n][m]将为具有连续布局的二维数组分配空间。5方便起见,包括对聚合初始化的变量声明可以省略最外层维度的大小,如果可以从聚合的内容推断出该信息:

Despite the interoperability of pointers and arrays in C, programmers need to be aware that the two are not the same, particularly in the context of variable declarations, which need to allocate space when elaborated. The declaration of a pointer variable allocates space to hold a pointer, while the declaration of an array variable allocates space to hold the whole array. In the case of an array the declaration must specify a size for each dimension. Thus int *a[n], when elaborated, will allocate space for n row pointers; int a[n][m] will allocate space for a two-dimensional array with contiguous layout.5 As a convenience, a variable declaration that includes initialization to an aggregate can omit the size of the outermost dimension if that information can be inferred from the contents of the aggregate:

int a [][2] = {{1, 2}, {3, 4}, {5, 6}}; // 三行

int a [][2] = {{1, 2}, {3, 4}, {5, 6}}; // three rows

设计与实现

Design & Implementation

8.8 指针和数组

8.8 Pointers and arrays

许多 C 程序使用指针而不是下标来迭代数组元素。在开发现代优化编译器之前,基于指针的数组遍历通常用于消除冗余地址计算,从而加快代码速度。然而,对于现代编译器,情况可能正好相反:冗余地址计算可以被识别为公共子表达式,并且某些其他代码改进对于索引比对于指针更容易。特别是,正如我们将在第 17 章中看到的那样,指针使得代码改进者更难确定两个左值何时可能是另一个的别名。

Many C programs use pointers instead of subscripts to iterate over the elements of arrays. Before the development of modern optimizing compilers, pointer-based array traversal often served to eliminate redundant address calculations, thereby leading to faster code. With modern compilers, however, the opposite may be true: redundant address calculations can be identified as common subexpressions, and certain other code improvements are easier for indices than they are for pointers. In particular, as we shall see in Chapter 17, pointers make it significantly more difficult for the code improver to determine when two l-values may be aliases for one other.

如今,指针算法的使用主要取决于个人喜好:一些 C 程序员认为基于指针的算法比基于数组的算法更优雅,而另一些人则认为基于数组的算法更难读。当然,数组作为指针传递这一事实使得以指针风格编写子例程变得很自然。

Today the use of pointer arithmetic is mainly a matter of personal taste: some C programmers consider pointer-based algorithms to be more elegant than their array-based counterparts, while others find them harder to read. Certainly the fact that arrays are passed as pointers makes it natural to write subroutines in the pointer style.

例 8.47

Example 8.47

C 语言中的数组作为参数

Arrays as parameters in C

当函数调用的参数列表中包含数组时,C 会传递指向数组第一个元素的指针,而不是数组本身。对于一维整数数组,相应的形式参数可以声明为int a[]int *a。对于具有行指针布局的二维整数数组,形式参数可以声明为int *a[]int **a。对于具有连续布局的二维数组,形式参数可以声明为int a[][m]int (*a)[m]。第一维的大小无关紧要;传递的只是一个指针,并且 C 不执行动态检查以确保引用在数组的范围内。■

When an array is included in the argument list of a function call, C passes a pointer to the first element of the array, not the array itself. For a one-dimensional array of integers, the corresponding formal parameter may be declared as int a[] or int *a. For a two-dimensional array of integers with row-pointer layout, the formal parameter maybe declared as int *a[] or int **a. For a two-dimensional array with contiguous layout, the formal parameter maybe declared as int a[][m] or int (*a)[m]. The size of the first dimension is irrelevant; all that is passed is a pointer, and C performs no dynamic checks to ensure that references are within the bounds of the array. ■

在所有情况下,声明必须允许编译器(或人类读者)确定数组元素的大小,或者换句话说,指针引用的对象的大小。因此,int a[][]int (*a)[]都不是有效的变量或参数声明:它们都没有为编译器提供生成a + i 或 a[i]代码所需的大小信息。

In all cases, a declaration must allow the compiler (or human reader) to determine the size of the elements of an array or, equivalently, the size of the objects referred to by a pointer. Thus neither int a[][] nor int (*a)[] is a valid variable or parameter declaration: neither provides the compiler with the size information it needs to generate code for a + i or a[i].

例 8.48

Example 8.48

C 语言中的sizeof

sizeof in C

内置的sizeof运算符返回对象或类型的字节大小。当给定一个数组作为参数时,它将返回整个数组的大小。当给定一个指针作为参数时,它将返回指针本身的大小。如果a是一个数组,sizeof(a) / sizeof(a[0])将返回数组中元素的数量。同样,如果指针占用 4 个字节,双精度浮点数占用 8 个字节,那么给定

The built-in sizeof operator returns the size in bytes of an object or type. When given an array as argument it returns the size of the entire array. When given a pointer as argument it returns the size of the pointer itself. If a is an array, sizeof(a) / sizeof(a[0]) returns the number of elements in the array. Similarly, if pointers occupy 4 bytes and double-precision floating-point numbers occupy 8 bytes, then given

双*a;/* 指向双精度数的指针 */
双(* b)[10];/* 指向包含 10 个双精度数的数组的指针 */

我们有sizeof(a) = sizeof(b) = 4、sizeof(*a) = sizeof(*b[0]) = 8 和sizeof(*b) = 80。在大多数情况下,sizeof可以在编译时进行评估。主要例外是可变长度数组,其大小可能直到详述时才知道:

we have sizeof(a) = sizeof(b) = 4, sizeof(*a) = sizeof(*b[0]) = 8, and sizeof(*b) = 80. In most cases, sizeof can be evaluated at compile time. The principal exception occurs for variable-length arrays, whose size may not be known until elaboration time:

无效的f(int len){

void f(int len) {

int A[len]; /*sizeof(A) == len * sizeof(int) */

int A[len]; /* sizeof(A) == len * sizeof(int) */

08-01-9780124104099检查你的理解

Check Your Understanding

17. 说出三种对字符串提供特别广泛支持的语言。

17. Name three languages that provide particularly extensive support for character strings.

18. 为什么一种语言允许对字符串进行操作,而不允许对数组进行操作?

18. Why might a language permit operations on strings that it does not provide for arrays?

19. 使用位向量表示集合的优点和缺点是什么? 集合还能用其他什么方式实现?

19. What are the strengths and weaknesses of the bit-vector representation for sets? How else might sets be implemented?

20. 讨论在具有变量参考模型的语言中自然出现的指针和递归类型之间的权衡。

20. Discuss the tradeoffs between pointers and the recursive types that arise naturally in a language with a reference model of variables.

21. 总结各种编程语言中取消引用指针的方式。

21. Summarize the ways in which one dereferences a pointer in various programming languages.

22. 指针地址有什么区别?指针和引用有什么区别?

22. What is the difference between a pointer and an address? Between a pointer and a reference?

23. 讨论C中指针和数组互操作的优点和缺点。

23. Discuss the advantages and disadvantages of the interoperability of pointers and arrays in C.

24. 在什么情况下必须在 C 数组声明中指定其边界?

24. Under what circumstances must the bounds of a C array be specified in its declaration?

8.5.2 悬垂引用

8.5.2 Dangling References

例 8.49

Example 8.49

显式存储回收

Explicit storage reclamation

当堆分配的对象不再有效时,长时间运行的程序需要回收该对象的空间。堆栈对象作为子例程调用序列的一部分自动回收。如何回收堆对象?有两种方法。C、C++ 和 Rust 等语言要求程序员明确回收对象。例如,在 C 中,我们说free(my_ptr);在 C++ 中,我们说 delete my_ptr。C++ 提供了额外的功能:在回收空间之前,它会自动调用用户为该对象提供的任何析构函数。析构函数可以回收子对象的空间、从索引或表中删除对象、打印消息或在对象生命周期结束时执行任何其他适当的操作。■

When a heap-allocated object is no longer live, a long-running program needs to reclaim the object's space. Stack objects are reclaimed automatically as part of the subroutine calling sequence. How are heap objects reclaimed? There are two alternatives. Languages like C, C++, and Rust require the programmer to reclaim an object explicitly. In C, for example, one says free(my_ptr); in C++, delete my_ptr. C++ provides additional functionality: prior to reclaiming the space, it automatically calls any user-provided destructor function for the object. A destructor can reclaim space for subsidiary objects, remove the object from indices or tables, print messages, or perform any other operation appropriate at the end of the object's lifetime. ■

例 8.50

Example 8.50

在 C++ 中对堆栈变量的悬垂引用

Dangling reference to a stack variable in C++

悬垂引用是不再指向有效对象的活动指针。在 C 和 C++ 等允许程序员创建指向堆栈对象的指针的语言中,当子例程返回时可能会创建悬垂引用,而更大范围内的某个指针仍指向该子例程的本地对象:

A dangling reference is a live pointer that no longer points to a valid object. In languages like C and C++, which allow the programmer to create pointers to stack objects, a dangling reference may be created when a subroutine returns while some pointer in a wider scope still refers to a local object of that subroutine:

int i = 3;

int i = 3;

int *p = &i;

int *p = &i;

void foo() { int n = 5; p = &n; }

void foo() { int n = 5; p = &n; }

cout << *p; // 打印 3

cout << *p;     // prints 3

foo();

foo();

cout << *p; // 未定义行为:n 不再有效

cout << *p;     // undefined behavior: n is no longer live

例 8.51

Example 8.51

在 C++ 中对堆变量的悬垂引用

Dangling reference to a heap variable in C++

在具有显式回收堆对象的语言中,每当程序员回收指针仍然引用的对象时,就会创建一个悬空引用:

In a language with explicit reclamation of heap objects, a dangling reference is created whenever the programmer reclaims an object to which pointers still refer:

int *p = 新的 int;
*p = 3;
输出<< *p;// 打印 3
删除p;
输出<< *p;// 未定义行为:*p 已被回收

请注意,即使回收操作将其参数更改为空指针,也无法解决问题,因为其他指针可能仍引用同一对象。■

Note that even if the reclamation operation were to change its argument to a null pointer, this would not solve the problem, because other pointers might still refer to the same object. ■

由于语言实现可能会重用已回收的堆栈和堆对象的空间,因此使用悬垂引用的程序可能会读取或写入内存中现已属于其他对象的位。它甚至可能会修改现已属于实现的簿记信息的位,从而破坏堆栈或堆的结构。

Because a language implementation may reuse the space of reclaimed stack and heap objects, a program that uses a dangling reference may read or write bits in memory that are now part of some other object. It may even modify bits that are now part of the implementation's bookkeeping information, corrupting the structure of the stack or heap.

Algol 68 通过禁止指针指向任何生存期比指针本身更短的对象来解决对堆栈对象的悬垂引用问题。不幸的是,这条规则很难执行。除其他事项外,由于指针和指针可能引用的对象都可以作为参数传递给子例程,因此只有当引用参数附带隐藏的生存期指示时,动态语义检查才有可能。Ada 有一个更严格的规则,更容易执行:它禁止指针指向任何生存期比指针类型的生存期更短的对象。

Algol 68 addressed the problem of dangling references to stack objects by forbidding a pointer from pointing to any object whose lifetime was briefer than that of the pointer itself. Unfortunately, this rule is difficult to enforce. Among other things, since both pointers and objects to which pointers might refer can be passed as arguments to subroutines, dynamic semantic checks are possible only if reference parameters are accompanied by a hidden indication of lifetime. Ada has a more restrictive rule that is easier to enforce: it forbids a pointer from pointing to any object whose lifetime is briefer than that of the pointer's type.

08-02-9780124104099 更深入地

IN MORE DEPTH

在配套网站上,我们考虑了两种有时用于在运行时捕获悬空引用的机制。墓碑在每次指针访问时引入了额外的间接层。回收对象时,间接字(墓碑)将被标记,以使对该对象的未来引用无效。钥匙会向堆中的每个指针和每个对象添加一个字;这些字必须匹配,指针才有效。墓碑可用于允许指向非堆对象的指针的语言,但它们引入了回收墓碑本身的第二个问题。锁和钥匙稍微简单一些,但它们只适用于堆中的对象。

On the companion site we consider two mechanisms that are sometimes used to catch dangling references at run time. Tombstones introduce an extra level of indirection on every pointer access. When an object is reclaimed, the indirection word (tombstone) is marked in a way that invalidates future references to the object. Locks and keys add a word to every pointer and to every object in the heap; these words must match for the pointer to be valid. Tombstones can be used in languages that permit pointers to nonheap objects, but they introduce the secondary problem of reclaiming the tombstones themselves. Locks and keys are somewhat simpler, but they work only for objects in the heap.

8.5.3 垃圾收集

8.5.3 Garbage Collection

显式回收堆对象对程序员来说是一个沉重的负担,也是导致错误(内存泄漏和悬空引用)的主要来源。跟踪对象生存期所需的代码使程序的设计、实现和维护更加困难。一个有吸引力的替代方案是让语言实现通知对象何时不再有用并自动回收它们。自动回收(也称为垃圾收集)对于函数式语言或多或少是必不可少的:删除是一种非常命令式的操作,并且从函数构造和返回任意对象的能力意味着许多在命令式语言中在堆栈上分配的对象必须从函数式语言中的堆中分配,以赋予它们无限的范围。

Explicit reclamation of heap objects is a serious burden on the programmer and a major source of bugs (memory leaks and dangling references). The code required to keep track of object lifetimes makes programs more difficult to design, implement, and maintain. An attractive alternative is to have the language implementation notice when objects are no longer useful and reclaim them automatically. Automatic reclamation (otherwise known as garbage collection) is more or less essential for functional languages: delete is a very imperative sort of operation, and the ability to construct and return arbitrary objects from functions means that many objects that would be allocated on the stack in an imperative language must be allocated from the heap in a functional language, to give them unlimited extent.

随着时间的推移,自动垃圾收集在命令式语言中也变得流行起来。Java、C#、Scala、Go 和所有主流脚本语言中都有这种功能。自动收集很难实现,但与实现后程序员享受的便利相比,这种困难微不足道。自动收集也往往比手动回收慢,尽管它消除了检查悬空引用的需要。

Over time, automatic garbage collection has become popular for imperative languages as well. It can be found in, among others, Java, C#, Scala, Go, and all the major scripting languages. Automatic collection is difficult to implement, but the difficulty pales in comparison to the convenience enjoyed by programmers once the implementation exists. Automatic collection also tends to be slower than manual reclamation, though it eliminates any need to check for dangling references.

引用计数

Reference Counts

什么时候对象不再有用?一个可能的答案是:当没有指向它的指针时。6简单的垃圾收集技术只是在每个对象中放置一个计数器,以跟踪引用该对象的指针数量。创建对象时,此引用计数设置为 1,以表示指针由新操作返回。当一个指针被赋值给另一个指针时,运行时系统会减少赋值左侧先前引用的对象(如果有)的引用计数,并增加右侧引用的对象的计数。在子程序返回时,调用序列结尾必须减少即将被销毁的本地指针引用的任何对象的引用计数。当引用计数达到零时,可以回收其对象。递归地,运行时系统必须减少被回收对象内指针引用的任何对象的计数,如果它们的计数达到零,则回收这些对象。为了防止收集器跟踪垃圾地址,每个指针必须在阐述时初始化为空。

When is an object no longer useful? One possible answer is: when no pointers to it exist.6 The simplest garbage collection technique simply places a counter in each object that keeps track of the number of pointers that refer to the object. When the object is created, this reference count is set to one, to represent the pointer returned by the new operation. When one pointer is assigned into another, the run-time system decrements the reference count of the object (if any) formerly referred to by the assignment's left-hand side, and increments the count of the object referred to by the right-hand side. On subroutine return, the calling sequence epilogue must decrement the reference count of any object referred to by a local pointer that is about to be destroyed. When a reference count reaches zero, its object can be reclaimed. Recursively, the run-time system must decrement counts for any objects referred to by pointers within the object being reclaimed, and reclaim those objects if their counts reach zero. To prevent the collector from following garbage addresses, each pointer must be initialized to null at elaboration time.

设计与实现

Design & Implementation

8.9 垃圾收集

8.9 Garbage collection

垃圾收集在便利性和安全性与性能之间进行了经典的权衡。由应用程序正确实现的手动存储回收几乎总是比任何自动垃圾收集器都快。它也更可预测:自动收集因其在实时或交互式程序的执行中引入间歇性“故障”而臭名昭著。

Garbage collection presents a classic tradeoff between convenience and safety on the one hand and performance on the other. Manual storage reclamation, implemented correctly by the application program, is almost invariably faster than any automatic garbage collector. It is also more predictable: automatic collection is notorious for its tendency to introduce intermittent “hiccups” in the execution of real-time or interactive programs.

Ada 采取了不同寻常的立场,拒绝表明立场:语言设计使自动垃圾收集成为可能,但实现不需要提供它,程序员可以使用名为Unchecked_Deallocation的内置例程请求手动回收。该语言的较新版本提供了广泛的功能,程序员可以实现自己的存储管理器(垃圾收集或不收集),不同类型的指针对应于不同的存储“池”。

Ada takes the unusual position of refusing to take a stand: the language design makes automatic garbage collection possible, but implementations are not required to provide it, and programmers can request manual reclamation with a built-in routine called Unchecked_Deallocation. Newer versions of the language provide extensive facilities whereby programmers can implement their own storage managers (garbage collected or not), with different types of pointers corresponding to different storage “pools.”

同样,Java 实时规范允许程序员创建所谓的作用域内存区域,这些区域仅供当前运行的线程子集访问。当所有可以访问给定区域的线程终止时,该区域将被全部回收。垃圾收集器永远不会检查作用域内存区域中分配的对象;因此,可以通过为每个实时线程提供作用域内存来避免因垃圾收集而导致的性能异常。

In a similar vein, the Real Time Specification for Java allows the programmer to create so-called scoped memory areas that are accessible to only a subset of the currently running threads. When all threads with access to a given area terminate, the area is reclaimed in its entirety. Objects allocated in a scoped memory area are never examined by the garbage collector; performance anomalies due to garbage collection can therefore be avoided by providing scoped memory to every real-time thread.

为了使引用计数发挥作用,语言实现必须能够识别每个指针的位置。当子程序返回时,它必须能够分辨出堆栈框架中的哪些字代表指针;当堆中的对象被回收时,它必须能够分辨出对象中的哪些字代表指针。跟踪此信息的标准技术依赖于编译器生成的类型描述符。程序中每个不同类型都有一个描述符,每个子程序的堆栈框架都有一个描述符,全局变量集也有一个描述符。大多数描述符只是一个表,列出了可以找到指针的类型内的偏移量,以及这些指针引用的对象类型的描述符地址。对于标记变体记录(可区分联合)类型,描述符稍微复杂一些:它必须包含标记的值(或范围)列表,以及相应变体的表。对于未标记的变体记录,没有可接受的解决方案:引用计数仅在语言是强类型时才有效(但请参阅第 8.5.3 节末尾的“保守收集”的讨论)。

In order for reference counts to work, the language implementation must be able to identify the location of every pointer. When a subroutine returns, it must be able to tell which words in the stack frame represent pointers; when an object in the heap is reclaimed, it must be able to tell which words within the object represent pointers. The standard technique to track this information relies on type descriptors generated by the compiler. There is one descriptor for every distinct type in the program, plus one for the stack frame of each subroutine, and one for the set of global variables. Most descriptors are simply a table that lists the offsets within the type at which pointers can be found, together with the addresses of descriptors for the types of the objects referred to by those pointers. For a tagged variant record (discriminated union) type, the descriptor is a bit more complicated: it must contain a list of values (or ranges) for the tag, together with a table for the corresponding variant. For untagged variant records, there is no acceptable solution: reference counts work only if the language is strongly typed (but see the discussion of “Conservative Collection” at the end of Section 8.5.3).

例 8.52

Example 8.52

引用计数和循环结构

Reference counts and circular structures

引用计数最重要的问题源于其对“有用对象”的定义。虽然如果不存在对某个对象的引用,该对象就是无用的,但是当存在引用时,该对象也可能是无用的。如图8.14所示,引用计数可能无法收集循环结构。它们只适用于保证非循环的结构。许多语言实现对可变长度字符串使用引用计数;字符串从不包含对其他任何内容的引用。Perl 对所有动态分配的数据使用引用计数;手册警告程序员在不再需要数据时手动中断循环。一些纯函数式语言也可能能够在所有情况下安全地使用引用计数,如果缺少赋值语句可以防止它们引入循环。最后,引用计数可用于回收墓碑。虽然确实可以创建带有墓碑的循环结构,但是程序员负责显式地释放堆对象这一事实意味着,仅当程序员无法回收其引用的对象时,引用计数才会无法回收墓碑。■

The most important problem with reference counts stems from their definition of a “useful object.” While it is definitely true that an object is useless if no references to it exist, it may also be useless when references do exist. As shown in Figure 8.14, reference counts may fail to collect circular structures. They work well only for structures that are guaranteed to be noncircular. Many language implementations use reference counts for variable-length strings; strings never contain references to anything else. Perl uses reference counts for all dynamically allocated data; the manual warns the programmer to break cycles manually when data aren't needed anymore. Some purely functional languages may also be able to use reference counts safely in all cases, if the lack of an assignment statement prevents them from introducing circularity. Finally, reference counts can be used to reclaim tombstones. While it is certainly possible to create a circular structure with tombstones, the fact that the programmer is responsible for explicit deallocation of heap objects implies that reference counts will fail to reclaim tombstones only when the programmer has failed to reclaim the objects to which they refer. ■

传真:08-14-9780124104099
图 8.14 引用计数和循环列表。此处显示的列表无法通过任何程序变量找到,但由于它是循环的,因此每个单元都包含一个非零计数。
智能指针

通用术语“智能指针”是指程序级对象(在语言本身之上实现),它模仿指针的行为,但具有额外的语义。智能指针最常见的用途是在通常仅支持手动存储回收的语言中实现引用计数。其他用途包括指针算法的边界检查、调试或性能分析的检测以及对外部对象(例如打开的文件)的引用的跟踪。

The general term smart pointer refers to a program-level object (implemented on top of the language proper) that mimics the behavior of a pointer, but with additional semantics. The most common use of smart pointers is to implement reference counting in a language that normally supports only manual storage reclamation. Other uses include bounds checking on pointer arithmetic, instrumentation for debugging or performance analysis, and tracking of references to external objects—e.g., open files.

在 C++ 标准库中可以找到对智能指针的特别丰富的支持,其unique_ptr、shared_ptrweak_ptr类利用运算符重载、构造函数、析构函数和移动语义来简化原本困难的手动回收任务。unique_ptr顾名思义就是对象的唯一引用。如果unique_ptr被销毁(通常是因为声明它的函数返回),那么它指向的对象将被指针的析构函数回收,如第8.5.2 节中所述。如果将一个unique_ptr赋值给另一个 unique_ptr(或作为参数传递),则重载的赋值运算符或构造函数会通过将旧指针更改为null来转移指向对象的所有权。(移动语义,我们将在第 9.3.1 节的“C++ 中的引用”中更详细地描述,通常允许编译器优化所有权转移的成本。)

Particularly rich support for smart pointers can be found in the C++ standard library, whose unique_ptr, shared_ptr, and weak_ptr classes leverage operator overloading, constructors, destructors, and move semantics to simplify the otherwise difficult task of manual reclamation. A unique_ptr is what its name implies—the only reference to an object. If the unique_ptr is destroyed (typically because the function in which it was declared returns), then the object to which it points is reclaimed by the pointer's destructor, as suggested in Section 8.5.2. If one unique_ptr is assigned into another (or passed as a parameter), the overloaded assignment operator or constructor transfers ownership of the pointed-to object by changing the old pointer to null. (Move semantics, which we will describe in more detail in under “References in C++” in Section 9.3.1, often allow the compiler to optimize away the cost of the ownership transfer.)

shared_ptr类型为指向的对象实现引用计数,通常将其存储在隐藏的、类似墓碑的中间对象中。计数在shared_ptr构造函数中递增,在析构函数中递减,并调整(双向)通过赋值操作。当需要循环结构时,或者当程序员想要维护簿记信息而又不想人为延长对象生命周期时,可以使用weak_ptr指向对象而不参与引用计数。当没有指向该对象的shared_ptr时,C++库将回收该对象;任何剩余的weak_ptr随后将表现得像它们为null一样。

The shared_ptr type implements a reference count for the pointed-to object, typically storing it in a hidden, tombstone-like intermediate object. Counts are incremented in shared_ptr constructors, decremented in destructors, and adjusted (in both directions) by assignment operations. When circular structures are required, or when the programmer wants to maintain bookkeeping information without artificially extending object lifetimes, a weak_ptr can be used to point to an object without contributing to reference counting. The C++ library will reclaim an object when no shared_ptr to it remains; any remaining weak_ptrs will subsequently behave as if they were null.

追踪收藏

Tracing Collection

正如我们所见,引用计数将对象定义为有用的,只要存在指向它的指针。更好的定义可能是说,如果可以通过从具有名称的某个对象(即堆外的某个对象)开始的一系列有效指针到达某个对象,则该对象是有用的。根据此定义,图 8.14下半部分中的块是无用的,即使它们的引用计数非零。跟踪收集器通过从外部指针开始递归探索堆来确定什么是有用的。

As we have seen, reference counting defines an object to be useful if there exists a pointer to it. A better definition might say that an object is useful if it can be reached by following a chain of valid pointers starting from something that has a name (i.e., something outside the heap). According to this definition, the blocks in the bottom half of Figure 8.14 are useless, even though their reference counts are nonzero. Tracing collectors work by recursively exploring the heap, starting from external pointers, to determine what is useful.

标记-清除

根据这个更准确的定义,识别无用块的经典机制称为标记和清除。它分为三个主要步骤,当堆中剩余的可用空间量低于某个最低阈值时,由垃圾收集器执行:

The classic mechanism to identify useless blocks, under this more accurate definition, is known as mark-and-sweep. It proceeds in three main steps, executed by the garbage collector when the amount of free space remaining in the heap falls below some minimum threshold:

1. 收集器遍历整个堆,暂时将每个块标记为“无用”。

1. The collector walks through the heap, tentatively marking every block as “useless.”

2. 从堆外的所有指针开始,收集器以递归方式探索程序中所有链接的数据,将每个新发现的块标记为“有用”。 (当遇到已经标记为“有用”的块时,收集器知道它已经通过先前的路径到达该块,并且无需递归即可返回。)

2. Beginning with all pointers outside the heap, the collector recursively explores all linked data in the program, marking each newly discovered block as “useful.” (When it encounters a block that is already marked as “useful,” the collector knows it has reached the block over some previous path, and returns without recursing.)

3. 收集器再次遍历堆,将每个仍然标记为“无用”的块移到空闲列表中。

3. The collector again walks through the heap, moving every block that is still marked “useless” to the free list.

设计与实现

Design & Implementation

8.10 垃圾到底是什么?

8.10 What exactly is garbage?

引用计数隐式地将垃圾对象定义为不存在任何指针的对象。跟踪隐式地将其定义为无法从堆外部访问的对象。理想情况下,我们希望有一个更严格的定义:垃圾对象是程序永远不会再使用的对象。我们满足于不可达性,因为这个理想定义是不可判定的。这种差异在实践中很重要:如果程序维护一个指向它永远不会再使用的对象的指针,那么垃圾收集器将无法回收它。如果此类对象的数量随着时间的推移而增长,那么尽管存在垃圾收集器,但程序仍存在内存泄漏。(我们可以简单地想象一个程序将每个新分配的对象添加到全局列表中,但从未真正浏览过该列表。这样的程序将完全击败收集器。)

Reference counting implicitly defines a garbage object as one to which no pointers exist. Tracing implicitly defines it as an object that is no longer reachable from outside the heap. Ideally, we'd like an even stronger definition: a garbage object is one that the program will never use again. We settle for non-reachability because this ideal definition is undecidable. The difference can matter in practice: if a program maintains a pointer to an object it will never use again, then the garbage collector will be unable to reclaim it. If the number of such objects grows with time, then the program has a memory leak, despite the presence of a garbage collector. (Trivially we could imagine a program that added every newly allocated object to a global list, but never actually perused the list. Such a program would defeat the collector entirely.)

为了提高空间效率,建议程序员“清零”不再需要的任何指针。这样做可能很困难,但不如完全手动回收那么困难——特别是,我们不需要意识到何时将指向给定对象的最后一个指针清零。出于同样的原因,悬空引用永远不会出现:垃圾收集器将避免回收任何可沿其他路径到达的对象。

For the sake of space efficiency, programmers are advised to “zero out” any pointers they no longer need. Doing this can be difficult, but not as difficult as fully manually reclamation—in particular, we do not need to realize when we are zeroing the last pointer to a given object. For the same reason, dangling references can never arise: the garbage collector will refrain from reclaiming any object that is reachable along some other path.

该算法的几个潜在问题显而易见。首先,无论是初始还是最终遍历堆,都要求收集器能够分辨出每个“正在使用”的块的开始和结束位置。在具有可变大小堆块的语言中,每个块都必须以指示其大小以及当前是否空闲开始。其次,收集器必须能够在步骤 2 中找到每个块中包含的指针。标准解决方案是将指向类型描述符的指针放置在每个块的开头附近。

Several potential problems with this algorithm are immediately apparent. First, both the initial and final walks through the heap require that the collector be able to tell where every “in-use” block begins and ends. In a language with variable-size heap blocks, every block must begin with an indication of its size, and of whether it is currently free. Second, the collector must be able in Step 2 to find the pointers contained within each block. The standard solution is to place a pointer to a type descriptor near the beginning of each block.

指针反转

例 8.53

Example 8.53

带指针反转的堆跟踪

Heap tracing with pointer reversal

标记-清除收集的探索步骤(步骤 2)自然是递归的。显而易见的实现需要一个堆栈,其最大深度与堆中的最长链成比例。实际上,这个堆栈的空间可能不可用:毕竟,我们在空间即将用完时才会运行垃圾收集!7探索步骤的另一种实现使用了 Schorr 和 Waite [ SW67 ]首次提出的技术,将堆栈的等效物嵌入堆块中已经存在的字段中。更具体地说,当收集器探索到给定块的路径时,它会反转它所遵循的指针,以便每个指针都指向前一个块而不是指向下一个块。图 8.15说明了这种指针反转技术。在探索过程中,收集器会跟踪当前块以及它来自的块。

The exploration step (Step 2) of mark-and-sweep collection is naturally recursive. The obvious implementation needs a stack whose maximum depth is proportional to the longest chain through the heap. In practice, the space for this stack may not be available: after all, we run garbage collection when we're about to run out of space!7 An alternative implementation of the exploration step uses a technique first suggested by Schorr and Waite [SW67] to embed the equivalent of the stack in already-existing fields in heap blocks. More specifically, as the collector explores the path to a given block, it reverses the pointers it follows, so that each points back to the previous block instead of forward to the next. This pointer-reversal technique is illustrated in Figure 8.15. As it explores, the collector keeps track of the current block and the block from whence it came.

传真:08-15-9780124104099
图 8.15 通过指针反转探索堆。当前正在检查的块由 curr 指针指示。前一个块由 prev 指针指示。当垃圾收集器从一个块移动到下一个块时,它会更改其所跟随的指针以指向前一个块。当它返回到某个块时,它会恢复指针。必须标记每个反转指针(用阴影框表示),以将其与同一块中的其他前向指针区分开来。

要从块X返回到块U(在图中的 (d) 部分之后),收集器将使用U中的反转指针来恢复其前一个块(T)的概念。然后,它将反转指针翻转回X,并将其当前块的概念更新为U。如果它返回的块包含其他指针,收集器将再次向前推进;否则,它将返回前一个反转指针并重试。在任何给定时间,每个块中最多只有一个指针会被反转。必须标记此指针,可能通过每个块开头的另一个簿记字段。(我们可以通过设置其一个低位来标记指针,但时间成本可能过高:我们每次访问时都必须搜索该块。)■

To return from block X to block U (after part (d) of the figure), the collector will use the reversed pointer in U to restore its notion of previous block (T). It will then flip the reversed pointer back to X and update its notion of current block to U. If the block to which it has returned contains additional pointers, the collector will proceed forward again; otherwise it will return across the previous reversed pointer and try again. At most one pointer in every block will be reversed at any given time. This pointer must be marked, probably by means of another bookkeeping field at the beginning of each block. (We could mark the pointer by setting one of its low-order bits, but the cost in time would probably be prohibitive: we'd have to search the block on every visit.) ■

停止并复制

在具有可变大小堆块的语言中,垃圾收集器可以通过执行存储压缩来减少外部碎片。许多垃圾收集器采用一种称为停止和复制的技术,该技术在实现压缩的同时消除了标准标记和清除算法中的步骤 1 和 3。具体来说,它们将堆分成两个大小相等的区域。所有分配都发生在前半部分。当这一半(几乎)已满时,收集器开始探索可到达的数据结构。每个可到达的块都被复制到堆后半部分的连续位置,没有外部碎片。堆前半部分中块的旧版本被“有用”标志和指向新位置的指针覆盖。指向同一块的任何其他指针(并在探索的后期找到)都被设置为指向新位置。当收集器完成探索时,所有有用的对象都已移动(并压缩)到后半部分堆的一半,前半部分不再需要。因此,收集器可以交换前半部分和后半部分的概念,程序可以继续。显然,这种算法的缺点是,在任何给定时间只能使用一半的堆,但在具有虚拟内存的系统中,只有虚拟空间未得到充分利用;堆的每个“一半”可以根据需要占用大部分物理内存。此外,通过消除标准标记和清除的第 1 步和第 3 步,停止和复制产生的开销与非垃圾块的数量成比例,而不是与总块数成比例。

In a language with variable-size heap blocks, the garbage collector can reduce external fragmentation by performing storage compaction. Many garbage collectors employ a technique known as stop-and-copy that achieves compaction while simultaneously eliminating Steps 1 and 3 in the standard mark-and-sweep algorithm. Specifically, they divide the heap into two regions of equal size. All allocation happens in the first half. When this half is (nearly) full, the collector begins its exploration of reachable data structures. Each reachable block is copied into contiguous locations in the second half of the heap, with no external fragmentation. The old version of the block, in the first half of the heap, is overwritten with a “useful” flag and a pointer to the new location. Any other pointer that refers to the same block (and is found later in the exploration) is set to point to the new location. When the collector finishes its exploration, all useful objects have been moved (and compacted) into the second half of the heap, and nothing in the first half is needed anymore. The collector can therefore swap its notion of first and second halves, and the program can continue. Obviously, this algorithm suffers from the fact that only half of the heap can be used at any given time, but in a system with virtual memory it is only the virtual space that is underutilized; each “half” of the heap can occupy most of physical memory as needed. Moreover, by eliminating Steps 1 and 3 of standard mark-and-sweep, stop-and-copy incurs overhead proportional to the number of nongarbage blocks, rather than the total number of blocks.

世代收藏

为了进一步降低跟踪收集的成本,一些垃圾收集器采用了“分代”技术,利用了大多数动态分配的对象寿命较短这一现象。堆被分成多个区域(通常是两个)。当空间不足时,收集器首先检查最年轻的区域(“托儿所”),它认为该区域可能包含最高比例的垃圾。只有当收集器无法回收此区域中的足够空间时,它才会检查下一个较旧的区域。为了避免在长期运行的系统中泄漏存储,收集器必须准备好在必要时检查整个堆。但是,在大多数情况下,收集的开销仅与最年轻区域的大小成正比。

To further reduce the cost of tracing collection, some garbage collectors employ a “generational” technique, exploiting the observation that most dynamically allocated objects are short lived. The heap is divided into multiple regions (often two). When space runs low the collector first examines the youngest region (the “nursery”), which it assumes is likely to have the highest proportion of garbage. Only if it is unable to reclaim sufficient space in this region does the collector examine the next-older region. To avoid leaking storage in long-running systems, the collector must be prepared, if necessary, to examine the entire heap. In most cases, however, the overhead of collection will be proportional to the size of the youngest region only.

设计与实现

Design & Implementation

8.11 引用计数与跟踪

8.11 Reference counts versus tracing

引用计数要求每个堆对象中都有一个计数器字段。对于诸如cons单元之类的小对象,这种空间开销可能很大。在具有大量指针操作的程序中,当指针更改时更新引用计数的持续开销也可能很大。然而,其他垃圾收集技术也有类似的开销。跟踪通常需要每个堆块中都有一个反向指针指示器,而引用计数则不需要,并且分代收集器通常必须在每次指针分配时产生开销,以便跟踪指向堆最新部分的指针。

Reference counts require a counter field in every heap object. For small objects such as cons cells, this space overhead may be significant. The ongoing expense of updating reference counts when pointers are changed can also be significant in a program with large amounts of pointer manipulation. Other garbage collection techniques, however, have similar overheads. Tracing generally requires a reversed pointer indicator in every heap block, which reference counting does not, and generational collectors must generally incur overhead on every pointer assignment in order to keep track of pointers into the newest section of the heap.

引用计数和跟踪之间的两个主要权衡是前者无法处理循环,而后者倾向于定期“停止世界”以回收空间。总体而言,实现者倾向于在不存在循环问题的应用程序中使用引用计数,并在一般情况下使用跟踪收集器。一些现实世界的系统将这两种方法混合使用,持续使用引用计数,偶尔使用跟踪收集来捕获任何循环结构。“停止世界”问题也可以通过增量并发收集器来解决,这些收集器的执行与程序的其余部分交错进行,但这些收集器往往具有更高的总开销。高效、有效的垃圾收集技术仍然是一个活跃的研究领域。

The two principal tradeoffs between reference counting and tracing are the inability of the former to handle cycles and the tendency of the latter to “stop the world” periodically in order to reclaim space. On the whole, implementors tend to favor reference counting for applications in which circularity is not an issue, and tracing collectors in the general case. Some real-world systems mix the two approaches, using reference counts on an ongoing basis, with an occasional tracing collection to catch any circular structures. The “stop the world” problem can also be addressed with incremental or concurrent collectors, which interleave their execution with the rest of the program, but these tend to have higher total overhead. Efficient, effective garbage collection techniques remain an active area of research.

任何在当前区域中幸存了少量收集(通常为一次)的对象都会被提升(移动)到下一个较旧的区域,其方式类似于停止和复制。当然,追踪托儿所需要将旧对象指向新对象的指针视为探索的外部“根”。同样,提升也需要更新从旧对象到新对象的指针以反映新位置。虽然旧空间到新空间的指针往往很少见,但分代收集器必须能够快速找到它们。在每次指针分配时,编译器都会生成代码来检查新值是否是旧到新指针;如果是,它会将指针添加到收集器可访问的隐藏列表中。这种分配上的检测称为写屏障。8

Any object that survives some small number of collections (often one) in its current region is promoted (moved) to the next older region, in a manner reminiscent of stop-and-copy. Tracing of the nursery requires, of course, that pointers from old objects to new objects we treated as external “roots” of exploration. Promotion likewise requires that pointers from old objects to new objects be updated to reflect the new locations. While old-space-to-new-space pointers tend to be rare, a generational collector must be able to find them all quickly. At each pointer assignment, the compiler generates code to check whether the new value is an old-to-new pointer; if so, it adds the pointer to a hidden list accessible to the collector. This instrumentation on assignments is known as a write barrier.8

保守收藏

语言实现者传统上认为,只有强类型语言才可能实现自动存储回收:引用计数和跟踪收集都要求我们能够找到对象内的指针。如果我们愿意承认某些垃圾不会被回收,那么我们可以实现标记-清除收集,而无需找到指针 [ BW88 ]。关键是要观察到堆中任何给定的块都跨越相对较少的地址。内存中某个非指针字恰好包含类似于其中一个地址的位模式的可能性非常小。

Language implementors have traditionally assumed that automatic storage reclamation is possible only in languages that are strongly typed: both reference counts and tracing collection require that we be able to find the pointers within an object. If we are willing to admit the possibility that some garbage will go unreclaimed, it turns out that we can implement mark-and-sweep collection without being able to find pointers [BW88]. The key is to observe that any given block in the heap spans a relatively small number of addresses. There is only a very small probability that some word in memory that is not a pointer will happen to contain a bit pattern that looks like one of those addresses.

如果我们保守地假设所有似乎指向堆块的东西实际上都是有效指针,那么我们可以继续进行标记和清除收集。当空间不足时,收集器(像往常一样)暂时将堆中的所有块标记为无用。然后,它会扫描堆栈和全局存储中的所有字对齐量。如果这些字中的任何一个似乎包含堆中某个东西的地址,收集器就会将包含该地址的块标记为有用。然后,收集器会递归扫描块中的所有字对齐量,并将在其中找到地址的任何其他块标记为有用。最后(像往常一样),收集器会回收仍然标记为无用的任何块。

If we assume, conservatively, that everything that seems to point into a heap block is in fact a valid pointer, then we can proceed with mark-and-sweep collection. When space runs low, the collector (as usual) tentatively marks all blocks in the heap as useless. It then scans all word-aligned quantities in the stack and in global storage. If any of these words appears to contain the address of something in the heap, the collector marks the block that contains that address as useful. Recursively, the collector then scans all word-aligned quantities in the block, and marks as useful any other blocks whose addresses are found therein. Finally (as usual), the collector reclaims any blocks that are still marked useless.

只要程序员不“隐藏”指针,该算法就是完全安全的(从某种意义上说,它永远不会回收有用的块)。例如,在 C 语言中,如果程序员将指针转换为int,然后其与常量进行异或,并期望稍后恢复和使用该指针,则收集器不太可能正常运行。除了有时会留下无人认领的垃圾外,保守收集还存在无法执行压缩的问题:收集器永远无法确定应该更改哪些“指针”。

The algorithm is completely safe (in the sense that it never reclaims useful blocks) so long as the programmer never “hides” a pointer. In C, for example, the collector is unlikely to function correctly if the programmer casts a pointer to int and then xors it with a constant, with the expectation of restoring and using the pointer at a later time. In addition to sometimes leaving garbage unclaimed, conservative collection suffers from the inability to perform compaction: the collector can never be sure which “pointers” should be changed.

08-01-9780124104099检查你的理解

Check Your Understanding

25. 什么是悬垂引用? 它们是如何产生的?为什么它们会成为问题?

25. What are dangling references?. How are they created, and why are they a problem?

26. 什么是垃圾?它是如何产生的?为什么它是一个问题?讨论引用计数跟踪收集作为解决问题的手段的比较优势。

26. What is garbage? How is it created, and why is it a problem? Discuss the comparative advantages of reference counts and tracing collection as a means of solving the problem.

27. 什么是智能指针?它们有什么用途?

27. What are smart pointers? What purpose do they serve?

28. 总结标记-清除、停止-复制和分代垃圾收集之间的区别。

28. Summarize the differences among mark-and-sweep, stop-and-copy, and generational garbage collection.

29. 什么是指针反转?它解决了什么问题?

29. What is pointer reversal? What problem does it address?

30. 什么是“保守”垃圾收集?它是如何工作的?

30. What is “conservative” garbage collection? How does it work?

31. 在同一编程语言中,是否会出现悬垂引用和垃圾?为什么会出现或为什么不会出现?

31. Do dangling references and garbage ever arise in the same programming language? Why or why not?

32. 为什么命令式编程语言采用自动垃圾收集功能的速度如此之慢?

32. Why was automatic garbage collection so slow to be adopted by imperative programming languages?

33. 允许指针引用不在堆中的对象有哪些优点和缺点?

33. What are the advantages and disadvantages of allowing pointers to refer to objects that do not lie in the heap?

8.6 列表

8.6 Lists

列表以递归方式定义为空列表或由初始对象(可以是列表或原子)和另一个(较短)列表组成的对。列表非常适合用函数式和逻辑语言进行编程,这些语言的大部分工作都是通过递归和高阶函数完成的(将在11.6 节中介绍)。

A list is defined recursively as either the empty list or a pair consisting of an initial object (which may be either a list or an atom) and another (shorter) list. Lists are ideally suited to programming in functional and logic languages, which do most of their work via recursion and higher-order functions (to be described in Section 11.6).

列表也可以用于命令式程序。一些传统的编译语言(例如 Clu)和大多数现代脚本语言都支持列表的内置类型构造函数。面向对象语言的库类也普遍支持列表,程序员可以使用任何带有记录和指针的语言来构建自己的列表。由于许多标准列表操作往往会产生垃圾,因此列表在具有自动垃圾收集功能的语言中效果最佳。

Lists can also be used in imperative programs. They are supported by built-in type constructors in a few traditional compiled languages (e.g., Clu) and in most modern scripting languages. They are also commonly supported by library classes in object-oriented languages, and programmers can build their own in any language with records and pointers. Since many of the standard list operations tend to generate garbage, lists tend to work best in a language with automatic garbage collection.

例 8.54

Example 8.54

ML 和 Lisp 中的列表

Lists in ML and Lisp

在两个主要的函数式语言系列中,列表的一个关键方面非常不同。ML 中的列表是同质的:列表的每个元素都必须具有相同的类型。相比之下,Lisp 列表是异构的:任何对象都可以放在列表中,只要它永远不会以不一致的方式使用即可。9这些不同的方法不同,实现也不同。ML 列表通常是块的链,每个块包含一个元素和一个指向下一个块的指针。Lisp 列表是cons单元的链,每个单元包含两个指针,一个指向元素,一个指向下一个cons单元(参见图 8.118.12)。由于历史原因,cons单元中的两个指针称为carcdr;它们分别代表列表的头和剩余元素。在语义(同质性与异质性)和实现(链式块与cons单元)方面,Clu 类似于 ML,而 Python 和 Prolog(将在第 12.2 节中讨论)类似于 Lisp。■

One key aspect of lists is very different in the two main functional language families. Lists in ML are homogeneous: every element of the list must have the same type. Lisp lists, by contrast, are heterogeneous: any object may be placed in a list, so long as it is never used in an inconsistent fashion.9 These different approaches lead to different implementations. An ML list is usually a chain of blocks, each of which contains an element and a pointer to the next block. A Lisp list is a chain of cons cells, each of which contains two pointers, one to the element and one to the next cons cell (see Figures 8.11 and 8.12). For historical reasons, the two pointers in a cons cell are known as the car and the cdr; they represent the head of the list and the remaining elements, respectively. In both semantics (homogeneity vs heterogeneity) and implementation (chained blocks vs cons cells), Clu resembles ML, while Python and Prolog (to be discussed in Section 12.2) resemble Lisp. ■

例 8.55

Example 8.55

列表符号

List notation

ML 和 Lisp 都提供了方便的列表符号。在 ML 的 OCaml 方言中,列表括在方括号中,元素用分号分隔:[a; b; c; d]。Lisp 列表括在圆括号中,元素用空格分隔:(abcd)。在这两种情况下,该符号表示一个列表 —— 其最内层的对由最后一个元素和空列表组成。在 Lisp 中,也可以构造一个列表,其最后一对包含两个元素。(严格来说,这样的列表不符合标准递归定义。)Lisp 系统提供了一种更通用但繁琐的点分列表符号,可以捕获真列表和非真列表。点分列表要么是一个原子(可能为空),要么是两个用句点分隔并用圆括号括起来的点分列表组成的对。点列表(a . (b . (c . (d . null))))与(abcd)相同。列表(a . (b . (c . d)))是不合适的;其最后的cons单元在第二个位置包含一个指向d的指针,而通常需要指向列表的指针。■

Both ML and Lisp provide convenient notation for lists. In the OCaml dialect of ML, a list is enclosed in square brackets, with elements separated by semicolons: [a; b; c; d]. A Lisp list is enclosed in parentheses, with elements separated by white space: (a b c d). In both cases, the notation represents a proper list—one whose innermost pair consists of the final element and the empty list. In Lisp, it is also possible to construct an improper list, whose final pair contains two elements. (Strictly speaking, such a list does not conform to the standard recursive definition.) Lisp systems provide a more general, but cumbersome dotted list notation that captures both proper and improper lists. A dotted list is either an atom (possibly null) or a pair consisting of two dotted lists separated by a period and enclosed in parentheses. The dotted list (a . (b . (c . (d . null)))) is the same as (a b c d). The list (a . (b . (c . d))) is improper; its final cons cell contains a pointer to d in the second position, where a pointer to a list is normally required. ■

ML 和 Lisp 都提供了丰富的内置多态函数来操作任意列表。由于程序在 Lisp 中就是列表,因此 Lisp 必须区分要评估的列表和要“保持原样”的列表。作为结构。为了防止对文字列表进行求值,Lisp 程序员可以引用它:(quote (abcd)),缩写为 ' (ab cd)。要评估内部列表(例如,函数返回的列表),程序员可以将其传递给内置函数eval。在 ML 中,程序不是列表,因此文字列表始终是结构聚合。

Both ML and Lisp provide a wealth of built-in polymorphic functions to manipulate arbitrary lists. Because programs are lists in Lisp, Lisp must distinguish between lists that are to be evaluated and lists that are to be left “as is,” as structures. To prevent a literal list from being evaluated, the Lisp programmer may quote it: (quote (a b c d)), abbreviated '(ab c d). To evaluate an internal list (e.g., one returned by a function), the programmer may pass it to the built-in function eval. In ML, programs are not lists, so a literal list is always a structural aggregate.

设计与实现

Design & Implementation

8.12汽车cdr

8.12 car and cdr

函数carcdr的名称是历史的偶然:它们源自 MIT 的 IBM 704 上 Lisp 的原始实现(1959 年)。该机器架构包括一些(36 位)循环控制指令中的 15 位“地址”和“减量”字段,以及用于从 36 位内存字中的这些字段之一加载索引寄存器或将其存储到这些字段的附加指令。Lisp 解释器的设计者决定让cons单元模仿指令的内部格式,以便他们可以利用这些特殊指令。在现在过时的用法中,内存字也称为“寄存器”。因此,可能适当地称为“第一”和“其余”指针的东西被称为 CAR 寄存器地址字段的内容)和CDR(寄存器减量字段的内容)。顺便说一句,704 也是 Fortran 首次开发的机器,也是第一台包含硬件浮点和磁芯存储器的商用机器。

The names of the functions car and cdr are historical accidents: they derive from the original (1959) implementation of Lisp on the IBM 704 at MIT. The machine architecture included 15-bit “address” and “decrement” fields in some of the (36-bit) loop-control instructions, together with additional instructions to load an index register from, or store it to, one of these fields within a 36-bit memory word. The designers of the Lisp interpreter decided to make cons cells mimic the internal format of instructions, so they could exploit these special instructions. In now archaic usage, memory words were also known as “registers.” What might appropriately have been called “first” and “rest” pointers thus came to be known as the CAR (contents of address field of register) and CDR (contents of decrement field of register). The 704, incidentally, was also the machine on which Fortran was first developed, and the first commercial machine to include hardware floating-point and magnetic core memory.

例 8.56

Example 8.56

Lisp 中的基本列表操作

Basic list operations in Lisp

列表最基本的操作是从列表的组件构造列表或从列表中提取列表的组件。在 Lisp 中:

The most fundamental operations on lists are those that construct them from their components or extract their components from them. In Lisp:

(反对‘a’(b))⇒ (ab)
(汽车'(ab))⇒ 一个
(汽车无)⇒ ??
(cdr'(abc))⇒ (公元前)
(cdr'(a))⇒ 无
(无指挥官)⇒ ??
(附加'(ab)'(cd))⇒ (abcd)

这里我们使用⇒ 来表示“计算为”。空列表(nil)的carcdr在Common Lisp 中定义为nil;在Scheme 中它们会导致动态语义错误。■

Here we have used ⇒ to mean “evaluates to.” The car and cdr of the empty list (nil) are defined to be nil in Common Lisp; in Scheme they result in a dynamic semantic error. ■

例 8.57

Example 8.57

OCaml 中的基本列表操作

Basic list operations in OCaml

在 OCaml 中等效操作写如下:

In OCaml the equivalent operations are written as follows:

一个::[b]⇒ [a; b]
hd [a, b]⇒ 一个
高清 []运行时异常
tl [a, b, c]⇒ [b,c]
tl [一个]⇒ []
tl []运行时异常
[a,b] @ [c,d]⇒ [a; b; c; d]

如果需要,程序可以捕获运行时异常;更多详细信息将在第 9.4 节中介绍。■

Run-time exceptions maybe caught by the program if desired; further details will appear in Section 9.4. ■

ML 和 Lisp 都提供了许多附加的列表函数,包括测试列表是否为空;返回列表的长度;返回列表的第n 个元素,或返回由除前n 个元素之外的所有元素组成的列表;反转列表元素的顺序;在列表中搜索与某些谓词匹配的元素;或将函数应用于列表的每个元素,并将结果作为列表返回。

Both ML and Lisp provide many additional list functions, including ones that test a list to see if it is empty; return the length of a list; return the nth element of a list, or a list consisting of all but the first n elements; reverse the order of the elements of a list; search a list for elements matching some predicate; or apply a function to every element of a list, returning the results as a list.

例 8.58

Example 8.58

列表推导

List comprehensions

包括 Miranda、Haskell、Python 和 F# 在内的多种语言都提供了与 ML 类似的列表,但增加了一个重要的附加机制,称为列表推导。这些是从传统的数学集合符号改编而来的。常见形式包括表达式、枚举器和一个或多个过滤器。在 Haskell 中,以下表示小于 100 的所有奇数的平方列表:

Several languages, including Miranda, Haskell, Python, and F#, provide lists that resemble those of ML, but with an important additional mechanism, known as list comprehensions. These are adapted from traditional mathematical set notation. A common form comprises an expression, an enumerator, and one or more filters. In Haskell, the following denotes a list of the squares of all odd numbers less than 100:

[i*i | i <- [1..100], i'mod' 2 == 1]

[i*i | i <- [1..100], i 'mod' 2 == 1]

在 Python 中我们会写

In Python we would write

[i*i 对于 i 在范围(1,100)内,如果 i % 2 == 1]

[i*i for i in range(1, 100) if i % 2 == 1]

在 F# 中等效的是

In F# the equivalent is

[对于 i in 1..100 执行如果 i % 2 = 1 则产生 i*i]

[for i in 1..100 do if i % 2 = 1 then yield i*i]

所有这些都是为了捕捉数学

All of these are meant to capture the mathematical

{×|{1100}模式2=1}

{i×i|i{1,,100}imod2=1}

si11_e

当然,我们可以通过一系列适当的函数调用创建一个等效的列表。然而,列表推导式语法的简洁性有时可以产生非常优雅的程序(例如,参见练习 8.22)。■

We could of course create an equivalent list with a series of appropriate function calls. The brevity of the list comprehension syntax, however, can sometimes lead to remarkably elegant programs (see, e.g., Exercise 8.22). ■

8.7 文件和输入/输出

8.7 Files and Input/Output

输入/输出 (I/O) 设施允许程序与外界通信。在讨论这种通信时,通常要区分交互式I/O 和文件 I/O。交互式 I/O 通常意味着与人类用户或物理设备的通信,这些设备与正在运行的程序并行工作,并且它们对程序的输入可能取决于程序先前的输出(例如提示)。文件通常是指操作系统实现的离线存储。文件可能进一步分为临时文件和持久文件临时文件在单个程序运行期间存在;它们的目的是存储程序可用内存无法容纳的大量信息。持久文件允许程序读取程序开始运行前存在的数据,并写入程序结束后仍将继续存在的数据。

Input/output (I/O) facilities allow a program to communicate with the outside world. In discussing this communication, it is customary to distinguish between interactive I/O and I/O with files. Interactive I/O generally implies communication with human users or physical devices, which work in parallel with the running program, and whose input to the program may depend on earlier output from the program (e.g., prompts). Files generally refer to off-line storage implemented by the operating system. Files maybe further categorized into those that are temporary and those that are persistent. Temporary files exist for the duration of a single program run; their purpose is to store information that is too large to fit in the memory available to the program. Persistent files allow a program to read data that existed before the program began running, and to write data that will continue to exist after the program has ended.

I/O 是语言设计中最困难的方面之一,也是不同语言之间共性最小的方面。有些语言为 I/O提供内置文件数据类型和特殊语法结构。其他语言将 I/O 完全交给库包,这些库包导出(通常不透明的)文件类型和各种输入和输出子例程。语言集成的主要优势是能够使用非子例程调用语法,并执行库例程可能无法使用的操作(例如,对具有不同数量参数的子例程调用进行类型检查)。另一方面,纯基于库的 I/O 方法可能会使语言定义中出现大量“混乱”。

I/O is one of the most difficult aspects of a language to design, and one that displays the least commonality from one language to the next. Some languages provide built-in file data types and special syntactic constructs for I/O. Others relegate I/O entirely to library packages, which export a (usually opaque) file type and a variety of input and output subroutines. The principal advantage of language integration is the ability to employ non-subroutine-call syntax, and to perform operations (e.g., type checking on subroutine calls with varying numbers of parameters) that may not otherwise be available to library routines. A purely library-based approach to I/O, on the other hand, may keep a substantial amount of “clutter” out of the language definition.

08-02-9780124104099 更深入地

IN MORE DEPTH

可以在配套网站上找到语言级 I/O 机制的概述。在简要介绍交互式和基于文件的 I/O 之后,我们主要关注文本文件的常见情况。文本文件中的数据以字符形式存储形式,但在读写操作期间可以转换为内部类型或从内部类型转换为内部类型作为示例,我们考虑 Fortran、Ada、C 和 C++ 的文本 I/O 功能。

An overview of language-level I/O mechanisms can be found on the companion site. After a brief introduction to interactive and file-based I/O, we focus mainly on the common case of text files. The data in a text file are stored in character form, but may be converted to and from internal types during read and write operations. As examples, we consider the text I/O facilities of Fortran, Ada, C, and C++.

08-01-9780124104099检查你的理解

Check Your Understanding

34. 为什么列表在函数式编程语言中被如此广泛地使用?

34. Why are lists so heavily used in functional programming languages?

35. 什么是列表推导?哪些语言支持它们?

35. What are list comprehensions? What languages support them?

36. 比较并对比 ML 和 Lisp 系列语言对列表的支持。

36. Compare and contrast the support for lists in ML- and Lisp-family languages.

37.解释 交互式基于文件的I/O 以及临时文件和持久文件之间的区别。

37. Explain the distinction between interactive and file-based I/O; between temporary and persistent files.

38. 在语言本身中支持 I/O 与在库中支持 I/O 之间有哪些权衡?

38. What are some of the tradeoffs between supporting I/O in the language proper versus supporting it in libraries?

8.8 总结和结束语

8.8 Summary and Concluding Remarks

本节总结了我们关于语言设计的六个核心章节中的第四章(名称 [来自第一部分]、控制流、类型系统、复合类型、子例程和类)。在我们对复合类型的调查中,我们在记录、数组和递归类型上花费了最多的时间。记录的关键问题包括变体记录的语法和语义、整个记录操作、类型安全以及它们与内存布局的交互。内存布局对于数组也很重要,它与形状的绑定时间、静态、堆栈和基于堆的分配策略、数字应用程序中的高效数组遍历、C 中指针和数组的互操作性以及可用的整个数组和基于切片的操作集交互。

This section concludes the fourth of our six core chapters on language design (names [from Part I], control flow, type systems, composite types, subroutines, and classes). In our survey of composite types, we spent the most time on records, arrays, and recursive types. Key issues for records include the syntax and semantics of variant records, whole-record operations, type safety, and the interaction of each of these with memory layout. Memory layout is also important for arrays, in which it interacts with binding time for shape; static, stack, and heap-based allocation strategies; efficient array traversal in numeric applications; the interoperability of pointers and arrays in C; and the available set of whole-array and slice-based operations.

对于递归数据类型,很大程度上取决于变量/名称的模型和引用模型之间的选择。递归类型是引用模型的自然产物;对于值模型,它们需要指针的概念:一个值是引用的变量。从实现的角度来看,值和引用之间的区别很重要:将内置类型实现为引用会很浪费,因此具有引用模型的语言通常会以不同的方式实现内置类型和用户定义类型。Java 在语言语义中反映了这种区别,要求内置类型的值模型和用户定义类类型的对象的引用模型。

For recursive data types, much depends on the choice between the value and reference models of variables/names. Recursive types are a natural fallout of the reference model; with the value model they require the notion of a pointer: a variable whose value is a reference. The distinction between values and references is important from an implementation point of view: it would be wasteful to implement built-in types as references, so languages with a reference model generally implement built-in and user-defined types differently. Java reflects this distinction in the language semantics, calling for a value model of built-in types and a reference model for objects of user-defined class types.

递归类型通常用于创建链接数据结构。在大多数情况下,这些结构必须从堆中分配。在某些语言中,程序员负责释放不再需要的堆对象。在其他语言中,语言运行时系统会自动识别和回收此类垃圾。显式释放对程序员来说是一种负担,并且会导致内存泄漏悬空引用的问题。虽然语言实现几乎从不尝试捕获内存泄漏(但是,请参阅探索 3.34练习 C-8.28 ,了解有关此主题的一些想法),但有时会使用墓碑钥匙来捕获悬空引用。自动垃圾收集可能很昂贵,但事实证明这种技术越来越受欢迎。大多数垃圾收集技术要么依赖于引用计数,要么依赖于对当前可访问结构的某种形式的递归探索(跟踪)。后一类技术包括标记和清除、停止和复制以及分代收集。

Recursive types are generally used to create linked data structures. In most cases these structures must be allocated from a heap. In some languages, the programmer is responsible for deallocating heap objects that are no longer needed. In other languages, the language run-time system identifies and reclaims such garbage automatically. Explicit deallocation is a burden on the programmer, and leads to the problems of memory leaks and dangling references. While language implementations almost never attempt to catch memory leaks (see Exploration 3.34 and Exercise C-8.28, however, for some ideas on this subject) tombstones or locks and keys are sometimes used to catch dangling references. Automatic garbage collection can be expensive, but has proved increasingly popular. Most garbage-collection techniques rely either on reference counts or on some form of recursive exploration (tracing) of currently accessible structures. Techniques in this latter category include mark-and-sweep, stop-and-copy, and generational collection.

语言设计中很少有领域像 I/O 那样表现出如此多的变化。我们的讨论(主要在配套网站上)区分了交互式 I/O(它往往非常特定于平台)和基于文件的 I/O(它细分为临时文件,用于单个程序运行中的大量数据)和持久文件,用于离线存储。文件还细分为以模拟内存布局的二进制形式表示其信息的文件和与基于字符的文本相互转换的文件。与二进制文件相比,文本文件通常会产生时间和空间开销,但它们具有可移植性和人类可读性的重要优势。

Few areas of language design display as much variation as I/O. Our discussion (largely on the companion site) distinguished between interactive I/O, which tends to be very platform specific, and file-based I/O, which subdivides into temporary files, used for voluminous data within a single program run, and persistent files, used for off-line storage. Files also subdivide into those that represent their information in a binary form that mimics layout in memory and those that convert to and from character-based text. In comparison to binary files, text files generally incur both time and space overhead, but they have the important advantages of portability and human readability.

在对类型的考察中,我们看到了许多语言创新的例子,这些创新有助于提高程序的清晰度和可维护性,而且通常几乎没有性能开销。这些例子包括用户定义类型的原始想法(Algol 68)、枚举和子范围类型(​​Pascal)、记录和变体的集成(Pascal)以及 Ada 中子类型和派生类型之间的区别。在第 10 章中,我们将研究许多人认为过去 30 年最重要的语言创新,即面向对象。

In our examination of types, we saw many examples of language innovations that have served to improve the clarity and maintainability of programs, often with little or no performance overhead. Examples include the original idea of user-defined types (Algol 68), enumeration and subrange types (Pascal), the integration of records and variants (Pascal), and the distinction between subtypes and derived types in Ada. In Chapter 10 we will examine what many consider the most important language innovation of the past 30 years, namely object orientation.

与前面几章一样,我们看到了一些为了简化编译器或使编译后的程序更小或更快而牺牲语言的便利性、正交性或类型安全性的例子。例如,许多语言缺乏对记录的相等性测试,Pascal 和 Ada 要求记录的变体部分位于末尾,许多语言对集合的最大大小有限制,C 语言缺乏对 I/O 的类型检查,以及许多语言实现中普遍缺乏动态语义检查。我们还看到了一些语言特性的例子,这些特性至少部分是为了提高实现效率而引入的。这些特性包括打包类型、多长度数字类型、十进制算法和 C 样式指针算法。

As in previous chapters, we saw several cases in which a language's convenience, orthogonality, or type safety appears to have been compromised in order to simplify the compiler, or to make compiled programs smaller or faster. Examples include the lack of an equality test for records in many languages, the requirement in Pascal and Ada that the variant portion of a record lie at the end, the limitations in many languages on the maximum size of sets, the lack of type checking for I/O in C, and the general lack of dynamic semantic checks in many language implementations. We also saw several examples of language features introduced at least in part for the sake of efficient implementation. These include packed types, multilength numeric types, decimal arithmetic, and C-style pointer arithmetic.

同时,我们发现语言设计者和用户越来越愿意容忍语言实现的复杂性和成本,以改善语义。这里的例子包括 Ada 的类型安全变体记录;Java 和 C# 的标准长度数字类型;现代脚本语言的可变长度字符串和字符串运算符;Ada、C 和各种脚本语言中数组边界的后期绑定;以及 Fortran 90 中丰富的全数组和基于切片的数组操作。可能还包括 ML 及其后代的多态类型推断。当然,还应该包括自动垃圾收集的广泛采用。一旦对于生产质量命令式语言来说,垃圾收集被认为成本过高,现在它不仅是函数式和脚本语言的标准,而且是 Ada、Java、C#、Scala 和 Go 等语言的标准。

At the same time, one can identify a growing willingness on the part of language designers and users to tolerate complexity and cost in language implementation in order to improve semantics. Examples here include the type-safe variant records of Ada; the standard-length numeric types of Java and C#; the variable-length strings and string operators of modern scripting languages; the late binding of array bounds in Ada, C, and the various scripting languages; and the wealth of whole-array and slice-based array operations in Fortran 90. One might also include the polymorphic type inference of ML and its descendants. Certainly one should include the widespread adoption of automatic garbage collection. Once considered too expensive for production-quality imperative languages, garbage collection is now standard not only in functional and scripting languages, but in Ada, Java, C#, Scala, and Go, among others.

8.9 练习

8.9 Exercises

8.1 假设我们正在为一台具有 1 字节字符、2 字节短整型、4 字节整数和 8 字节实数的机器进行编译,并且其对齐规则要求每个原始数据元素的地址都是元素大小的偶数倍。进一步假设编译器不允许对字段进行重新排序。以下数组将占用多少空间?解释一下。A :记录数组 [0..9] s:短整型c:字符t:短整型d:字符r:实数i:整数

 

  

  

  

  

  

  

8.1 Suppose we are compiling for a machine with 1-byte characters, 2-byte shorts, 4-byte integers, and 8-byte reals, and with alignment rules that require the address of every primitive data element to be an even multiple of the element's size. Suppose further that the compiler is not permitted to reorder fields. How much space will be consumed by the following array? Explain.

 A : array [0..9] of record

  s : short

  c : char

  t : short

  d : char

  r : real

  i : integer

8.2 示例 8.10中,我们建议根据记录字段的对齐要求对其进行排序,以尽量减少空洞。在示例中,我们按最小对齐优先进行排序。如果我们按最长对齐优先进行排序,会发生什么情况?您认为这种方案有什么优点吗?有什么缺点吗?如果整个记录必须是最长对齐的偶数倍,这两种方法在所需的总空间上是否有差异?

8.2 In Example 8.10 we suggested the possibility of sorting record fields by their alignment requirement, to minimize holes. In the example, we sorted smallest-alignment-first. What would happen if we sorted longest-alignment-first? Do you see any advantages to this scheme? Any disadvantages? If the record as a whole must be an even multiple of the longest alignment, do the two approaches ever differ in total space required?

8.3 给出 Ada 代码,使用以下代码将小写字母映射到大写字母

8.3 Give Ada code to map from lowercase to uppercase letters, using

(一) 一个数组

(a) an array

(b) 函数

(b) a function

注意语法的相似性:在两种情况下,upper('a')都是'A'

Note the similarity of syntax: in both cases upper('a') is 'A'.

8.4 8.2.2 节中,我们注意到,在具有动态数组和变量值模型的语言中,记录可能具有在编译时未知大小的字段。为了适应这种情况,我们建议对记录使用一个 dope 向量来跟踪字段的偏移量。

假设我们想为每个字段维护一个静态偏移量。我们能否设计出一种受图8.7的堆栈框架布局启发的替代策略,并将每个记录分为固定大小部分和可变大小部分?我们需要解决哪些问题?(提示:考虑嵌套记录。)

8.4 In Section 8.2.2 we noted that in a language with dynamic arrays and a value model of variables, records could have fields whose size is not known at compile time. To accommodate these, we suggested using a dope vector for the record, to track the offsets of the fields.

Suppose instead that we want to maintain a static offset for each field. Can we devise an alternative strategy inspired by the stack frame layout of Figure 8.7, and divide each record into a fixed-size part and a variable-size part? What problems would we need to address? (Hint: Consider nested records.)

8.5 解释如何扩展图 8.7以适应按值传递的子程序参数,但其形状直到运行时调用子程序时才知道。

8.5 Explain how to extend Figure 8.7 to accommodate subroutine arguments that are passed by value, but whose shape is not known until the subroutine is called at run time.

8.6 解释如何使用 C 中的指针获得 Fortran 90 的 allocate 语句对一维数组的效果。您可能会发现您的解决方案不能推广到多维数组。为什么?如果您熟悉 C++,请说明如何使用其功能来解决问题。

8.6 Explain how to obtain the effect of Fortran 90's allocate statement for one-dimensional arrays using pointers in C. You will probably find that your solution does not generalize to multidimensional arrays. Why not? If you are familiar with C++, show how to use its class facilities to solve the problem.

8.7 示例 8.24考虑了二维字符数组的布局,仅计算了用于字符和指针的空间。如果空间是静态分配的,作为编译时已知的日期或关键字的全局数组,则这是合适的。相反,假设空间在堆中分配,每个连续存储块有 4 或 8 个字节的开销。这会如何改变空间效率的权衡?

8.7 Example 8.24, which considered the layout of a two-dimensional array of characters, counted only the space devoted to characters and pointers. This is appropriate if the space is allocated statically, as a global array of days or keywords known at compile time. Supposed instead that space is allocated in the heap, with 4 or 8 bytes of overhead for each contiguous block of storage. How does this change the tradeoffs in space efficiency?

8.8考虑 示例 8.25中的数组索引计算。假设i、jk已加载到寄存器中,并且A的元素是整数,在 32 位机器上连续分配在内存中。用侧栏 5.1 中的伪汇编符号显示将A[i, j, k]加载到寄存器的指令序列。您可以假设存在一种能够按 2 的小幂缩放的索引寻址模式。假设最终的内存加载是缓存命中,您的代码在现代处理器上可能需要多少个周期?

8.8 Consider the array indexing calculation of Example 8.25. Suppose that i, j, and k are already loaded into registers, and that A's elements are integers, allocated contiguously in memory on a 32-bit machine. Show, in the pseudo-assembly notation of Sidebar 5.1, the instruction sequence to load A[i, j, k] into a register. You may assume the existence of an indexed addressing mode capable of scaling by small powers of two. Assuming the final memory load is a cache hit, how many cycles is your code likely to require on a modern processor?

8.9 继续上一个练习,假设A具有行指针布局,并且i、jk再次在寄存器中可用。显示将A[i, j, k]加载到寄存器的伪汇编代码。假设所有内存加载都是缓存命中,您的代码在现代处理器上可能需要多少个周期?

8.9 Continuing the previous exercise, suppose that A has row-pointer layout, and that i, j, and k are again available in registers. Show pseudo-assembler code to load A[i, j, k] into a register. Assuming that all memory loads are cache hits, how many cycles is your code likely to require on a modern processor?

8.10 重复前两个练习,修改代码以包含数组下标边界的运行时检查。

8.10 Repeat the preceding two exercises, modifying your code to include runtime checking of array subscript bounds.

8.11 8.2.3 节中,我们讨论了如何区分数组引用的常量部分和变量部分,以便高效地访问数组和记录对象的子部分。另一种方法是生成简单代码,并依靠编译器的代码改进器来查找常量部分、将它们组合在一起并在编译时计算它们。讨论每种方法的优缺点。

8.11 In Section 8.2.3 we discussed how to differentiate between the constant and variable portions of an array reference, in order to efficiently access the subparts of array and record objects. An alternative approach is to generate naive code and count on the compiler's code improver to find the constant portions, group them together, and calculate them at compile time. Discuss the advantages and disadvantages of each approach.

8.12 考虑以下在 64 位 x86 机器上编译的 C 声明:struct { int n; char c; } A[10][10];如果A[0][0]的地址是 1000(十进制),那么 A[3][7]的地址是多少

 

  

  

 

8.12 Consider the following C declaration, compiled on a 64-bit x86 machine:

 struct {

  int n;

  char c;

 } A[10][10];

If the address of A[0][0] is 1000 (decimal), what is the address of A[3][7]?

8.13 假设我们在一台机器上为一种命令式语言生成代码,该机器有 8 字节浮点数、4 字节整数、1 字节字符,并且整数和浮点数都采用 4 字节对齐。假设此外,我们计划对多维数组使用连续的行主布局,我们不希望重新排序记录的字段或打包记录或数组,并且我们将假设所有数组下标都在范围内而不检查。

8.13 Suppose we are generating code for an imperative language on a machine with 8-byte floating-point numbers, 4-byte integers, 1-byte characters, and 4-byte alignment for both integers and floating-point numbers. Suppose further that we plan to use contiguous row-major layout for multidimensional arrays, that we do not wish to reorder fields of records or pack either records or arrays, and that we will assume without checking that all array subscripts are in bounds.

(a) 考虑以下变量声明:A : 实数数组 [1..10, 10..100] i : 整数x : 实数显示我们的编译器应为以下赋值生成的代码:x := A[3,i]。解释你是如何得出答案的。

 

 

 

(a) Consider the following variable declarations:

 A : array [1..10, 10..100] of real

 i : integer

 x : real

Show the code that our compiler should generate for the following assignment: x := A[3,i]. Explain how you arrived at your answer.

(b) 考虑以下更复杂的声明:r :记录x :整数y :字符A :记录数组 [1..10, 10..20] z :实数B :字符数组 [0..71] j , k :整数假设这些声明是当前子程序的本地声明。注意A中索引的下限;第一个元素是A[1,10]。描述r在内存中的布局方式。然后显示将rA[2,j].B[k]加载到寄存器的代码。务必指出地址计算的哪些部分可以在编译时执行。

 

  

  

  

   

   

 



(b) Consider the following more complex declarations:

 r : record

  x : integer

  y : char

  A : array [1..10, 10..20] of record

   z : real

   B : array [0..71] of char

 j, k : integer

Assume that these declarations are local to the current subroutine. Note the lower bounds on indices in A; the first element is A[1,10].

Describe how r would be laid out in memory. Then show code to load r.A[2,j].B[k] into a register. Be sure to indicate which portions of the address calculation could be performed at compile time.

8.14 假设A是一个 10×10 的(4 字节)整数数组,索引从[0][0][9][9] 。进一步假设A的地址当前在寄存器r1中,整数i的值当前在寄存器r2中,整数j的值当前在寄存器r3中。给出一个代码序列的伪汇编语言,该代码序列将A[i][j]

的值加载到寄存器r1中(a)假设A是使用(行主)连续分配实现的;(b)假设A是使用行指针实现的。伪代码的每一行都应对应于典型的现代机器上的一条指令。您可以根据需要使用任意数量的寄存器。您不需要保留r1、r2r3中的值。您可以假设ij在界限内,并且地址长度为 4 个字节。

哪个代码序列可能更快?为什么?

8.14 Suppose A is a 10×10 array of (4-byte) integers, indexed from [0][0] through [9][9]. Suppose further that the address of A is currently in register r1, the value of integer i is currently in register r2, and the value of integer j is currently in register r3.

Give pseudo-assembly language for a code sequence that will load the value of A[i][j] into register r1 (a) assuming that A is implemented using (row-major) contiguous allocation; (b) assuming that A is implemented using row pointers. Each line of your pseudocode should correspond to a single instruction on a typical modern machine. You may use as many registers as you need. You need not preserve the values in r1, r2, and r3. You may assume that i and j are in bounds, and that addresses are 4 bytes long.

Which code sequence is likely to be faster? Why?

8.15 指针和递归类型定义使确定类型结构等价性的算法变得复杂。例如,考虑以下定义:类型 A = 记录x : 指向 B 的指针y : 实数类型 B = 记录x : 指向 A 的指针y : 实数

 

  

  

 

  

  

7.2.1 节中给出的结构等价的简单定义(递归扩展子部分,直到只剩一串内置类型和类型构造函数;然后比较它们)不起作用:我们得到一个无限扩展(类型 A = 记录 x:指向记录的指针 x:指向记录的指针 x:指向记录的指针...)。显而易见的重新解释是说两个类型AB是等价的,如果任何字段选择、数组下标、指针取消引用和其他操作序列将一个人带入A的结构并以内置类型结束,总是遇到相同的字段名称,并且在用于深入B的结构时以相同的内置类型结束- 反之亦然。根据这种重新解释,上面的AB具有相同的类型。给出一个基于这种重新解释的算法,该算法可用于编译器中以确定结构等价。 (提示:最快的方法归功于 J. Král [ Krá73 ]。它基于用于查找接受给定正则语言的最小确定性有限自动机的算法。该算法在示例 2.15中概述;详细信息可在任何自动机理论教科书中找到[例如,[ HMU07 ]]。)

8.15 Pointers and recursive type definitions complicate the algorithm for determining structural equivalence of types. Consider, for example, the following definitions:

 type A = record

  x : pointer to B

  y : real

 type B = record

  x : pointer to A

  y : real

The simple definition of structural equivalence given in Section 7.2.1 (expand the subparts recursively until all you have is a string of built-in types and type constructors; then compare them) does not work: we get an infinite expansion (type A = record x : pointer to record x : pointer to record x : pointer to record …). The obvious reinterpretation is to say two types A and B are equivalent if any sequence of field selections, array subscripts, pointer dereferences, and other operations that takes one down into the structure of A, and that ends at a built-in type, always encounters the same field names, and ends at the same built-in type when used to dive into the structure of B—and vice versa. Under this reinterpretation, A and B above have the same type. Give an algorithm based on this reinterpretation that could be used in a compiler to determine structural equivalence. (Hint: The fastest approach is due to J. Král [Krá73]. It is based on the algorithm used to find the smallest deterministic finite automaton that accepts a given regular language. This algorithm was outlined in Example 2.15; details can be found in any automata theory textbook [e.g., [HMU07]].)

8.16 解释以下 C 声明的含义:double *a[n]; double (*b)[n]; double (*c[n])(); double (*d())[n];

 

 

 

 

8.16 Explain the meaning of the following C declarations:

 double *a[n];

 double (*b)[n];

 double (*c[n])();

 double (*d())[n];

8.17 在 Ada 83 中,指针(访问变量)只能指向堆中的对象。Ada 95 允许一种新的指针,即访问所有类型,也指向其他对象,前提是这些对象已声明为别名type int_ptr is access all Integer; foo : 别名 Integer; ip : int_ptr; ip := foo'Access; ' Access属性大致相当于 C 的“地址” (&)运算符。如何实现访问所有类型和别名对象?您的实现如何与堆中对象的自动垃圾收集(假设它存在)交互?

 

 

 

 

 

8.17 In Ada 83, pointers (access variables) can point only to objects in the heap. Ada 95 allows a new kind of pointer, the access all type, to point to other objects as well, provided that those objects have been declared to be aliased:

 type int_ptr is access all Integer;

 foo : aliased Integer;

 ip : int_ptr;

 

 ip := foo'Access;

The 'Access attribute is roughly equivalent to C's “address of” (&) operator. How would you implement access all types and aliased objects? How would your implementation interact with automatic garbage collection (assuming it exists) for objects in the heap?

8.18如 第 8.5.2 节所述,Ada 95 禁止访问所有指针引用任何生存期短于指针类型的对象。此规则可以在编译时完全执行吗?为什么或为什么不?

8.18 As noted in Section 8.5.2, Ada 95 forbids an access all pointer from referring to any object whose lifetime is briefer than that of the pointer's type. Can this rule be enforced completely at compile time? Why or why not?

8.19 在第 8.5 节中关于指针的大部分讨论中,我们隐式地假设每个指向堆的指针都指向动态分配的存储块的开头。在某些语言中,包括 Algol 68 和 C,指针也可能指向堆中块内的数据。如果您尝试实现悬垂引用的动态语义检查,或者自动垃圾收集(精确或保守),那么这种“内部指针”的存在会让您的任务变得多么复杂?

8.19 In much of the discussion of pointers in Section 8.5, we assumed implicitly that every pointer into the heap points to the beginning of a dynamically allocated block of storage. In some languages, including Algol 68 and C, pointers may also point to data inside a block in the heap. If you were trying to implement dynamic semantic checks for dangling references or, alternatively, automatic garbage collection (precise or conservative), how would your task be complicated by the existence of such “internal pointers”?

8.20 

8.20 

(a) 偶尔会有人建议垃圾收集语言应该提供删除操作作为优化:通过明确删除永远不会再使用的对象,程序员可以省去垃圾收集器自动查找和回收这些对象的麻烦,从而提高性能。你觉得这个建议怎么样?解释一下。

(a) Occasionally one encounters the suggestion that a garbage-collected language should provide a delete operation as an optimization: by explicitly delete-ing objects that will never be used again, the programmer might save the garbage collector the trouble of finding and reclaiming those objects automatically, thereby improving performance. What do you think of this suggestion? Explain.

(b) 或者,可以允许程序员“保有”某个对象,这样它就永远不会成为回收的候选对象。这是一个好主意吗?

(b) Alternatively, one might allow the programmer to “tenure” an object, so that it will never be a candidate for reclamation. Is this a good idea?

8.21 示例 8.52中,我们指出函数式语言可以安全地使用引用计数,因为没有赋值语句可以防止它们引入循环。这并不完全正确;像 Lisp letrec这样的构造也可用于产生循环,只要循环定义名称的使用隐藏在每个定义中的lambda表达式中:(define foo (lambda () (letrec ((a (lambda(f) (if f #\A b))) (b (lambda(f) (if f #\B c))) (c (lambda(f) (if f #\C a)))) a)))函数a、bc中的每一个都包含对下一个函数的引用:

 

  

   

    

    

   

8.21 In Example 8.52 we noted that functional languages can safely use reference counts since the lack of an assignment statement prevents them from introducing circularity. This isn't strictly true; constructs like the Lisp letrec can also be used to make cycles, so long as uses of circularly defined names are hidden inside lambda expressions in each definition:

 (define foo

  (lambda ()

   (letrec ((a (lambda(f) (if f #\A b)))

    (b (lambda(f) (if f #\B c)))

    (c (lambda(f) (if f #\C a))))

   a)))

Each of the functions a, b, and c contains a reference to the next:

((脚)⇒ #\A
(((foo)#f)#t)⇒ #\B
((((foo)#f)#f)#t)⇒ #\C
(((((foo)#f)#f)#f)#t)⇒ #\A

如何在不放弃引用计数的情况下解决这种循环问题?

How might you address this circularity without giving up on reference counts?

8.22 以下是 Haskell 中标准快速排序算法的框架:

quicksort [] = []

quicksort (a : l) = quicksort […] ++ [a] ++ quicksort […] ++

运算符表示列表连接(类似于ML 中的@)。:运算符相当于 ML 的::或 Lisp 的cons。说明如何将两个省略的表达式表示为列表推导。

8.22 Here is a skeleton for the standard quicksort algorithm in Haskell:

quicksort [] = []

quicksort (a : l) = quicksort […] ++ [a] ++ quicksort […]

The ++ operator denotes list concatenation (similar to @ in ML). The : operator is equivalent to ML's :: or Lisp's cons. Show how to express the two elided expressions as list comprehensions.

08-02-97801241040998.23–8.31 更深入。

8.23–8.31  In More Depth.

8.10 探索

8.10 Explorations

8.32 如果您可以使用提供可选动态语义检查的编译器,用于检查数组下标越界、记录变体使用不当和/或悬空或未初始化的指针,请试验这些检查的成本。它们会增加多少执行大量检查访问的程序的执行时间?试验不同级别的优化(代码改进),看看每种优化对检查开销有何影响。

8.32 If you have access to a compiler that provides optional dynamic semantic checks for out-of-bounds array subscripts, use of an inappropriate record variant, and/or dangling or uninitialized pointers, experiment with the cost of these checks. How much do they add to the execution time of programs that make a significant number of checked accesses? Experiment with different levels of optimization (code improvement) to see what effect each has on the overhead of checks.

8.33 编写一个库包,供语言实现用来管理从非常大的基类型(例如,整数)中提取的元素集。您应该支持成员资格测试、并集、交集和差集。您的包是否从堆中分配内存?如果是这样,假定使用您的包的编译器需要做什么来确保在不再需要时回收空间?

8.33 Write a library package that might be used by a language implementation to manage sets of elements drawn from a very large base type (e.g., integer). You should support membership tests, union, intersection, and difference. Does your package allocate memory from the heap? If so, what would a compiler that assumed the use of your package need to do to make sure that space was reclaimed when no longer needed?

8.34 了解 SETL [ SDDS86 ],这是一门基于集合的编程语言,由纽约大学的 Jack Schwartz 设计。列出作为内置集合操作提供的机制。将此列表与其他编程语言的集合功能进行比较。SETL 实现可能使用哪些数据结构来表示程序中的集合?

8.34 Learn about SETL [SDDS86], a programming language based on sets, designed by Jack Schwartz of New York University. List the mechanisms provided as built-in set operations. Compare this list with the set facilities of other programming languages. What data structure(s) might a SETL implementation use to represent sets in a program?

8.35  HotSpot Java 编译器和虚拟机实现了一整套垃圾收集器:传统的分代收集器、老生代压缩收集器、低暂停时间并行收集器、高吞吐量并行收集器、与主程序并行运行的“大多数并发”老生代收集器。详细了解这些算法。每种算法何时使用,为什么使用?

8.35 The HotSpot Java compiler and virtual machine implements an entire suite of garbage collectors: a traditional generational collector, a compacting collector for the old generation, a low pause-time parallel collector for the nursery, a high-throughput parallel collector for the old generation, and a “mostly concurrent” collector for the old generation that runs in parallel with the main program. Learn more about these algorithms. When is each used, and why?

8.36在 Ada 中实现您最喜欢的垃圾收集算法。或者,在 C++ 中实现 shared_ptr类的简化版本,该类的存储将被垃圾收集。您将需要使用模板(泛型),以便您的类可以针对任意指向的类型进行实例化。

8.36 Implement your favorite garbage collection algorithm in Ada. Alternatively, implement a simplified version of the shared_ptr class in C++, for which storage is garbage collected. You'll want to use templates (generics) so that your class can be instantiated for arbitrary pointed-to types.

8.37 用你最喜欢的语言实现来试验垃圾收集的成本。它使用哪种收集器?你能创建出它表现特别好或特别差的人工程序吗?

8.37 Experiment with the cost of garbage collection in your favorite language implementation. What kind of collector does it use? Can you create artificial programs for which it performs particularly well or poorly?

8.38 了解Java 中的弱引用。它们如何与垃圾回收交互?它们与 C++ 中的弱引用对象相比如何?描述它们可能有用的几种场景。

8.38 Learn about weak references in Java. How do they interact with garbage collection? How do they compare to weak_ptr objects in C++? Describe several scenarios in which they may be useful.

08-02-9780124104099 8.39–8.41 更深入。

8.39–8.41  In More Depth.

8.11 书目注释

8.11 Bibliographic Notes

虽然数组是最古老的复合数据类型,但它们仍然是语言设计中的一个活跃主题。2014 年 SIGPLAN 数组编程库、语言和编译器国际研讨会论文集 [ Hen14 ] 中可以找到具有代表性的当代作品。所有标准编译器文本都讨论了数组和记录的实现问题。Chamberlain 和 Snyder 描述了 ZPL 编程语言对稀疏数组的支持 [ CS01 ]。

While arrays are the oldest composite data type, they remain an active subject of language design. Representative contemporary work can be found in the proceedings of the 2014 SIGPLAN International Workshop on Libraries, Languages, and Compilers for Array Programming [Hen14]. Implementation issues for arrays and records are discussed in all the standard compiler texts. Chamberlain and Snyder describe support for sparse arrays in the ZPL programming language [CS01].

墓碑由 Lomet [ Lom75 , Lom85 ] 撰写。锁和钥匙由 Fischer 和 LeBlanc [ FL80 ] 撰写。后者还讨论了如何检查 Pascal 中的各种其他动态语义错误,包括变体记录中出现的错误。

Tombstones are due to Lomet [Lom75, Lom85]. Locks and keys are due to Fischer and LeBlanc [FL80]. The latter also discuss how to check for various other dynamic semantic errors in Pascal, including those that arise with variant records.

垃圾收集仍然是一个非常活跃的研究课题。许多正在进行的研究工作都在 ISMM(年度国际内存管理研讨会)上发表(www.sigplan.org/Conferences/ISMM)。常量空间(指针反转)标记-清除垃圾收集器由 Schorr 和 Waite [ SW67 ] 提出。停止-复制收集器由 Fenichel 和 Yochelson [ FY69 ] 开发,其思想基于 Minsky 的思想。Deutsch 和 Bobrow [ DB76 ] 描述了一种避免“stop-the-world”现象的增量式垃圾收集器。Wilson 和 Johnstone [ WJ93 ] 描述了一种较新的增量式收集器。第 8.5.3 节末尾描述的保守式收集器由 Boehm 和 Weiser [ BW88 ]提出。Cohen [ Coh81 ] 综述了截至 1981 年的垃圾收集技术;Wilson [ Wil92b ] 和 Jones 和 Lins [ JL96 ] 则提供了稍微新近的观点。 Bacon 等人 [ BCR04 ] 认为引用计数和跟踪实际上是同一底层存储问题的双重视角。

Garbage collection remains a very active topic of research. Much of the ongoing work is reported at ISMM, the annual International Symposium on Memory Management (www.sigplan.org/Conferences/ISMM). Constant-space (pointer-reversing) mark-and-sweep garbage collection is due to Schorr and Waite [SW67]. Stop-and-copy collection was developed by Fenichel and Yochelson [FY69], based on ideas due to Minsky. Deutsch and Bobrow [DB76] describe an incremental garbage collector that avoids the “stop-the-world” phenomenon. Wilson and Johnstone [WJ93] describe a later incremental collector. The conservative collector described at the end of Section 8.5.3 is due to Boehm and Weiser [BW88]. Cohen [Coh81] surveys garbage-collection techniques as of 1981; Wilson [Wil92b] and Jones and Lins [JL96] provide somewhat more recent views. Bacon et al. [BCR04] argue that reference counting and tracing are really dual views of the same underlying storage problem.


1相比之下,第 7.2.1 节“非转换类型转换”下提到的另一个示例(检查浮点数的内部结构)确实重新解释了位。在这种情况下也可以使用联合(练习 C-8.24),但在这里非转换类型转换更能表明意图。

1 By contrast, the other example mentioned under Nonconverting Type Casts in Section 7.2.1— examination of the internal structure of a floating-point number—does indeed reinterpret bits. Unions can also be used in this case (Exercise C-8.24), but here a nonconverting cast is a better indication of intent.

2肯尼斯·艾弗森(1920-2004),加拿大数学家,1954 年加入哈佛大学任教,在那里他设想了 APL 作为描述数学算法的符号。1960 年,他转入 IBM,在那里他帮助将 APL 开发成一种实用的编程语言。1970 年,他被任命为 IBM 院士,并于 1979 年获得 ACM 图灵奖。

2 Kenneth Iverson (1920–2004), a Canadian mathematician, joined the faculty at Harvard University in 1954, where he conceived APL as a notation for describing mathematical algorithms. He moved to IBM in 1960, where he helped develop the notation into a practical programming language. He was named an IBM Fellow in 1970, and received the ACM Turing Award in 1979.

3 “dope vector” 这个名称大概源于“在(某物)身上注射毒品”这一概念,这是起源于赛马的一个口语表达:提前得知一匹马被注射了毒品(“服用了兴奋剂”)对于下注来说有着重要意义,尽管这是不道德的。

3 The name “dope vector” presumably derives from the notion of “having the dope on (something),” a colloquial expression that originated in horse racing: advance knowledge that a horse has been drugged (“doped”) is of significant, if unethical, use in placing bets.

4 Fran Allen (1932-) 于 1957 年加入 IBM 的 TJ Watson 研究中心,并一直在那里工作直至职业生涯结束。她的开创性论文《程序优化》 [ All69 ] 推动了代码改进领域的发展。她于 20 世纪 80 年代初成立的 PTRAN(并行翻译)小组开发了自动并行化的大部分理论。1989 年,Allen 博士成为第一位被任命为 IBM 院士的女性。2006 年,她成为第一位获得 ACM 图灵奖的人。

4 Fran Allen (1932–) joined IBM's T. J. Watson Research Center in 1957, and stayed for her entire professional career. Her seminal paper, Program Optimization [All69] helped launch the field of code improvement. Her PTRAN (Parallel TRANslation) group, founded in the early 1980s, developed much of the theory of automatic parallelization. In 1989 Dr. Allen became the first woman to be named an IBM Fellow. In 2006 she became the first to receive the ACM Turing Award.

5要阅读 C 语言中的声明,遵循以下规则会很有帮助:从变量名称开始,尽可能向右,以括号为限;然后尽可能向左;然后跳出一层括号并重复。因此int *a[n]表示 a 是一个指向整数的n元素指针数组,而int (*a)[n]表示a是一个指向整数的n元素数组的指针。

5 To read declarations in C, it is helpful to follow the following rule: start at the name of the variable and work right as far as possible, subject to parentheses; then work left as far as possible; then jump out a level of parentheses and repeat. Thus int *a[n] means that a is an n-element array of pointers to integers, while int (*a)[n] means that a is a pointer to an n-element array of integers.

6在以下讨论中,我们将使用基于指针的术语来描述变量值模型语言。然而,这些技术同样适用于变量引用模型语言。

6 Throughout the following discussion we will use the pointer-based terminology of languages with a value model of variables. The techniques apply equally well, however, to languages with a reference model of variables.

7在许多语言实现中,堆栈和堆从内存的两端向对方增长(第 15.4 节);如果堆已满,堆栈就无法增长。在具有虚拟内存的系统中,两者之间的距离理论上可能非常大,但磁盘上备份它们的空间仍然有限,并且由它们共享。

7 In many language implementations, the stack and heap grow toward each other from opposite ends of memory (Section 15.4); if the heap is full, the stack can't grow. In a system with virtual memory the distance between the two may theoretically be enormous, but the space that backs them up on disk is still limited, and shared between them.

8不幸的是,“屏障”一词的含义过于繁琐。垃圾收集屏障与13.3.1 节中的同步屏障、 13.3.3 节中的内存屏障或C-15.2.1 节中的 RTL 屏障无关。

8 Unfortunately, the word “barrier” is heavily overloaded. Garbage collection barriers are unrelated to the synchronization barriers of Section 13.3.1, the memory barriers of Section 13.3.3, or the RTL barriers of Section C-15.2.1.

9回想一下,在 Lisp 中对象是自描述的。唯一的类型检查发生在函数“故意”检查参数以查看它是列表还是某种特定类型的原子时。

9 Recall that objects are self-descriptive in Lisp. The only type checking occurs when a function “deliberately” inspects an argument to see whether it is a list or an atom of some particular type.

9

子程序和控制抽象

Subroutines and Control Abstraction

在第 3 章的介绍中,我们定义了抽象 程序员可以将名称与可能复杂的程序片段关联起来,然后可以根据其目的或功能而不是其实现来思考该程序片段。我们有时会区分控制抽象和数据抽象,前者的主要目的是执行明确定义的操作,后者的主要目的是表示信息。1我们将在第 10 章中更详细地讨论数据抽象。

In the introduction to Chapter 3, we defined abstraction as a process by which the programmer can associate a name with a potentially complicated program fragment, which can then be thought of in terms of its purpose or function, rather than in terms of its implementation. We sometimes distinguish between control abstraction, in which the principal purpose of the abstraction is to perform a well-defined operation, and data abstraction, in which the principal purpose of the abstraction is to represent information.1 We will consider data abstraction in more detail in Chapter 10.

子程序是大多数编程语言中控制抽象的主要机制。子程序代表调用者执行操作调用者等待子程序完成后再继续执行。大多数子程序都是参数化的:调用者传递影响子程序行为的参数,或为其提供操作数据。参数也称为实际参数。它们在调用发生时映射到子程序的形式参数。返回值的子程序通常称为函数。不返回值的子程序通常称为过程。静态类型语言通常要求为每个被调用的子程序进行声明,以便编译器可以验证每个调用是否传递了正确数量和类型的参数。

Subroutines are the principal mechanism for control abstraction in most programming languages. A subroutine performs its operation on behalf of a caller, who waits for the subroutine to finish before continuing execution. Most subroutines are parameterized: the caller passes arguments that influence the subroutine's behavior, or provide it with data on which to operate. Arguments are also called actual parameters. They are mapped to the subroutine's formal parameters at the time a call occurs. A subroutine that returns a value is usually called a function. A subroutine that does not return a value is usually called a procedure. Statically typed languages typically require a declaration for every called subroutine, so the compiler can verify, for example, that every call passes the right number and types of arguments.

如第 3.2.2 节所述,在大多数语言中,参数和局部变量所消耗的存储空间可以分配在堆栈上。因此,我们从第 9.1 节开始本章,回顾堆栈的布局。然后,我们在第 9.2 节介绍用于维护此布局的调用序列。在此过程中,我们重新讨论了使用静态链访问嵌套子例程中的非局部变量,并考虑(在配套网站上)一种称为display 的替代机制,该机制具有类似的用途。我们还考虑了子例程内联和闭包的表示。为了说明一些可能的实现替代方案,我们(再次在配套网站上)提供了 LLVM 编译器的案例研究ARM 指令集和32 位和 64 位 x86 的gcc编译器。我们还讨论了SPARC 指令集的寄存器窗口机制。

As noted in Section 3.2.2, the storage consumed by parameters and local variables can in most languages be allocated on a stack. We therefore begin this chapter, in Section 9.1, by reviewing the layout of the stack. We then turn in Section 9.2 to the calling sequences that serve to maintain this layout. In the process, we revisit the use ofstatic chains to access nonlocal variables in nested subroutines, and consider (on the companion site) an alternative mechanism, known as a display, that serves a similar purpose. We also consider subroutine inlining and the representation of closures. To illustrate some of the possible implementation alternatives, we present (again on the companion site) case studies of the LLVM compiler for the ARM instruction set and the gcc compiler for 32- and 64-bit x86. We also discuss the register window mechanism of the SPARC instruction set.

第 9.3 节中,我们将更仔细地研究子程序参数。我们考虑参数传递模式,它决定了子程序可以对其形式参数应用的操作以及这些操作对相应实际参数的影响。我们还考虑了命名参数和默认参数、可变数量的参数以及函数返回机制。

In Section 9.3 we look more closely at subroutine parameters. We consider parameter-passing modes, which determine the operations that a subroutine can apply to its formal parameters and the effects of those operations on the corresponding actual parameters. We also consider named and default parameters, variable numbers of arguments, and function return mechanisms.

第 9.4 节中,我们考虑了异常情况的处理。虽然异常有时可以局限于当前子程序,但一般情况下,它们需要一种机制来“弹出”嵌套上下文而不返回,以便可以在调用上下文中进行恢复。在第 9.5 节中,我们考虑了协程,它允许程序维护两个或多个执行上下文,并在它们之间来回切换。协程可用于实现迭代器(第 6.5.3 节),但它们还有其他用途,特别是在模拟和服务器程序中。在第13 章中,我们将使用它们作为并发(“准并行”)线程的基础。最后,在第 9.6 节中,我们考虑异步事件- 发生在程序之外但程序需要对其作出响应的事情。

In Section 9.4, we consider the handling of exceptional conditions. While exceptions can sometimes be confined to the current subroutine, in the general case they require a mechanism to “pop out of” a nested context without returning, so that recovery can occur in the calling context. In Section 9.5, we consider coroutines, which allow a program to maintain two or more execution contexts, and to switch back and forth among them. Coroutines can be used to implement iterators (Section 6.5.3), but they have other uses as well, particularly in simulation and in server programs. In Chapter 13 we will use them as the basis for concurrent (“quasiparallel”) threads. Finally, in Section 9.6 we consider asynchronous events—things that happen outside a program, but to which it needs to respond.

9.1 堆栈布局回顾

9.1 Review of Stack Layout

例 9.1

Example 9.1

运行时堆栈的布局(重复)

Layout of run-time stack (reprise)

3.2.2 节中,我们讨论了子程序调用堆栈上的空间分配(图 3.1)。每个被调用的例程都会在堆栈顶部分配一个新的堆栈框架激活记录。此框架可能包含参数和/或返回值、簿记信息(包括返回地址和保存的寄存器)、局部变量和/或临时变量。当子程序返回时,其框架将从堆栈中弹出。■

In Section 3.2.2 we discussed the allocation of space on a subroutine call stack (Figure 3.1). Each routine, as it is called, is given a new stack frame, or activation record, at the top of the stack. This frame may contain arguments and/or return values, bookkeeping information (including the return address and saved registers), local variables, and/or temporaries. When a subroutine returns, its frame is popped from the stack. ■

例 9.2

Example 9.2

相对于帧指针的偏移量

Offsets from frame pointer

在任何给定时间,堆栈指针寄存器都包含堆栈顶部最后使用的位置或第一个未使用的位置的地址,具体取决于约定。帧指针寄存器包含帧内的地址。帧中的对象通过相对于帧指针的位移寻址来访问。如果在编译时不知道对象(例如,本地数组)的大小,则将该对象放置在帧顶部的可变大小区域中;其地址和内幕向量(描述符)存储在帧的固定大小部分中,与帧指针的偏移量是静态已知的(图 8.7)。如果没有可变大小的对象,则帧内的每个对象都具有与堆栈指针的静态已知偏移量,并且实现可以省去帧指针,从而释放寄存器用于其他用途。如果在编译时不知道参数的大小,则可以将该参数放置在帧中其他参数下方的可变大小部分中其地址和内幕向量与帧指针的偏移量是已知的。或者,调用者可以简单地传递一个临时地址和内幕向量,依靠被调用的例程将参数复制到框架顶部的可变大小区域中。■

At any given time, the stack pointer register contains the address of either the last used location at the top of the stack or the first unused location, depending on convention. The frame pointer register contains an address within the frame. Objects in the frame are accessed via displacement addressing with respect to the frame pointer. If the size of an object (e.g., a local array) is not known at compile time, then the object is placed in a variable-size area at the top of the frame; its address and dope vector (descriptor) are stored in the fixed-size portion of the frame, at a statically known offset from the frame pointer (Figure 8.7). If there are no variable-size objects, then every object within the frame has a statically known offset from the stack pointer, and the implementation may dispense with the frame pointer, freeing up a register for other use. If the size of an argument is not known at compile time, then the argument may be placed in a variable-size portion of the frame below the other arguments, with its address and dope vector at known offsets from the frame pointer. Alternatively, the caller may simply pass a temporary address and dope vector, counting on the called routine to copy the argument into the variable-size area at the top of the frame. ■

例 9.3

Example 9.3

静态和动态链接

Static and dynamic links

在具有嵌套子程序和静态作用域的语言(例如 Ada、Common Lisp、ML、Scheme 或 Swift)中,位于周围子程序中的对象,以及因此既不是局部的也不是全局的,可以通过维护静态链来找到(图 9.1)。每个堆栈框架都包含对词汇上包围子例程的框架的引用。此引用称为静态链接。类似地,框架指针的保存值(将在子例程返回时恢复)称为动态链接。静态链接和动态链接可能相同也可能不同,这取决于当前例程是由其词汇上包围的例程调用的,还是由嵌套在该包围例程中的其他例程调用的。■

In a language with nested subroutines and static scoping (e.g., Ada, Common Lisp, ML, Scheme, or Swift), objects that lie in surrounding subroutines, and that are thus neither local nor global, can be found by maintaining a static chain (Figure 9.1). Each stack frame contains a reference to the frame of the lexically surrounding subroutine. This reference is called the static link. By analogy, the saved value of the frame pointer, which will be restored on subroutine return, is called the dynamic link. The static and dynamic links may or may not be the same, depending on whether the current routine was called by its lexically surrounding routine, or by some other routine nested in that surrounding routine. ■

编号09-01-9780124104099
图 9.1 子程序嵌套示例,取自 图 3.5。在 B、C 和 D 中,所有五个例程都是可见的。在 A 和 E 中,例程 A、B 和 E 可见,但 C 和 D 不可见。给定调用序列 A、E、B、D、C,按照该顺序,帧将分配在堆栈上,如右图所示,并带有指示的静态和动态链接。

例 9.4

Example 9.4

嵌套例程的可见性

Visibility of nested routines

无论子例程是否被词汇上包围的例程直接调用,我们都可以肯定包围的例程是活动的;当前例程不可能是可见的,从而允许它被调用。例如,考虑图 9.1所示的子例程嵌套。如果子例程D直接从B调用,那么显然B的框架已经在堆栈上。否则还能如何调用D ?它在AE中不可见,因为它嵌套在B中。稍加思考就可以清楚地知道,只有当控制权进入B(将B的框架放在堆栈上)时, D才会出现。因此,它可以被C或嵌套在CD中的任何其他例程(未显示)调用,但仅仅是因为它们也在B内。■

Whether or not a subroutine is called directly by the lexically surrounding routine, we can be sure that the surrounding routine is active; there is no other way that the current routine could have been visible, allowing it to be called. Consider, for example, the subroutine nesting shown in Figure 9.1. If subroutine D is called directly from B, then clearly B's frame will already be on the stack. How else could D be called? It is not visible in A or E, because it is nested inside of B. A moment's thought makes clear that it is only when control enters B (placing B's frame on the stack) that D comes into view. It can therefore be called by C, or by any other routine (not shown) that is nested inside C or D, but only because these are also within B. ■

9.2 调用序列

9.2 Calling Sequences

维护子程序调用堆栈是调用序列(调用者在子程序调用之前和之后立即执行的代码)以及子程序本身的序言(在开头执行的代码)和尾声(在结尾执行的代码)的责任。有时术语“调用序列”用于指代调用者、序言和尾声的组合操作。

Maintenance of the subroutine call stack is the responsibility of the calling sequence—the code executed by the caller immediately before and after a subroutine call—and of the prologue (code executed at the beginning) and epilogue (code executed at the end) of the subroutine itself. Sometimes the term “calling sequence” is used to refer to the combined operations of the caller, the prologue, and the epilogue.

在进入子程序时必须完成的任务包括传递参数、保存返回地址、更改程序计数器、更改堆栈指针以分配空间、保存包含可能被调用方覆盖但仍在调用方中有效(可能需要)的值的寄存器(包括帧指针)、更改帧指针以引用新帧以及为新帧中需要它的任何对象执行初始化代码。在退出时必须完成的任务包括传递返回参数或函数值、为需要它的任何本地对象执行终结代码、释放堆栈帧(恢复堆栈指针)、恢复其他已保存的寄存器(包括帧指针)以及恢复程序计数器。其中一些任务(例如传递参数)必须由调用方执行,因为它们在不同的调用中有所不同。但是,大多数任务都可以由调用方或被调用方执行。一般来说,如果被调用者做尽可能多的工作,我们就会节省空间:被调用者中执行的任务在目标程序中只出现一次,但调用者中执行的任务会出现在每个调用站点,并且典型的子程序在多个地方被调用。

Tasks that must be accomplished on the way into a subroutine include passing parameters, saving the return address, changing the program counter, changing the stack pointer to allocate space, saving registers (including the frame pointer) that contain values that may be overwritten by the callee but are still live (potentially needed) in the caller, changing the frame pointer to refer to the new frame, and executing initialization code for any objects in the new frame that require it. Tasks that must be accomplished on the way out include passing return parameters or function values, executing finalization code for any local objects that require it, deallocating the stack frame (restoring the stack pointer), restoring other saved registers (including the frame pointer), and restoring the program counter. Some of these tasks (e.g., passing parameters) must be performed by the caller, because they differ from call to call. Most of the tasks, however, can be performed either by the caller or the callee. In general, we will save space if the callee does as much work as possible: tasks performed in the callee appear only once in the target program, but tasks performed in the caller appear at every call site, and the typical subroutine is called in more than one place.

保存和恢复寄存器

Saving and Restoring Registers

最棘手的分工问题可能与保存寄存器有关。理想的方法(参见 C-5.5.2 节)是精确保存那些在调用方中有效且在被调用方中用于其他目的的寄存器。然而,由于单独编译,很难(但并非不可能)确定这个相交集。一个更简单的解决方案是让调用方保存所有正在使用的寄存器,或者让被调用方保存它将覆盖的所有寄存器。

Perhaps the trickiest division-of-labor issue pertains to saving registers. The ideal approach (see Section C-5.5.2) is to save precisely those registers that are both live in the caller and needed for other purposes in the callee. Because of separate compilation, however, it is difficult (though not impossible) to determine this intersecting set. A simpler solution is for the caller to save all registers that are in use, or for the callee to save all registers that it will overwrite.

许多处理器(包括C-9.2.2 节案例研究中描述的 ARM 和 x86)的调用序列约定都达成了某种妥协:未保留用于特殊目的的寄存器被分成两组,大小大致相等。一组由调用者负责,另一组由被调用者负责。被调用者可以假设调用者保存集中的任何寄存器中都没有有价值的内容;调用者可以假设没有被调用者会破坏被调用者保存集中的任何寄存器的内容。为了节省代码大小,编译器尽可能使用被调用者保存的寄存器来保存局部变量和其他长期值。它使用调用者保存集来保存临时值,这些临时值不太可能在调用之间需要。这些约定的结果是,调用者保存寄存器很少被任何一方保存:被调用者知道它们是调用者的责任,并且调用者知道它们不包含任何重要内容。

Calling sequence conventions for many processors, including the ARM and x86 described in the case studies of Section C-9.2.2, strike something of a compromise: registers not reserved for special purposes are divided into two sets of approximately equal size. One set is the caller's responsibility, the other is the callee's responsibility. A callee can assume that there is nothing of value in any of the registers in the caller-saves set; a caller can assume that no callee will destroy the contents of any registers in the callee-saves set. In the interests of code size, the compiler uses the callee-saves registers for local variables and other long-lived values whenever possible. It uses the caller-saves set for transient values, which are less likely to be needed across calls. The result of these conventions is that the caller-saves registers are seldom saved by either party: the callee knows that they are the caller's responsibility, and the caller knows that they don't contain anything important.

维护静态链

Maintaining the Static Chain

在具有嵌套子例程的语言中,维护静态链所需的工作至少有一部分必须由调用者而不是被调用者执行,因为这项工作取决于调用者的词汇嵌套深度。标准方法是让调用者计算被调用者的静态链接并将其作为额外的隐藏参数传递。会出现两种子情况:

In languages with nested subroutines, at least part of the work required to maintain the static chain must be performed by the caller, rather than the callee, because this work depends on the lexical nesting depth of the caller. The standard approach is for the caller to compute the callee's static link and to pass it as an extra, hidden parameter. Two subcases arise:

1. 被调用者(直接)嵌套在调用者内部。在这种情况下,被调用者的静态链接应引用调用者的框架。因此,调用者将其自己的框架指针作为被调用者的静态链接传递。

1. The callee is nested (directly) inside the caller. In this case, the callee's static link should refer to the caller's frame. The caller therefore passes its own frame pointer as the callee's static link.

2. 被调用者的范围是“向外” k≥0——更接近词汇嵌套的外层。在这种情况下,围绕被调用者的所有范围也围绕调用者(否则被调用者将不可见)。调用者对其自己的静态链接进行k次解引用,并将结果作为被调用者的静态链接传递。

2. The callee is k ≥ 0 scopes “outward”—closer to the outer level of lexical nesting. In this case, all scopes that surround the callee also surround the caller (otherwise the callee would not be visible). The caller dereferences its own static link k times and passes the result as the callee's static link.

典型的调用序列

A Typical Calling Sequence

例 9.5

Example 9.5

典型的调用序列

A typical calling sequence

图 9.2显示了堆栈框架的一种合理布局,与图 3.1一致。堆栈指针 ( sp ) 指向堆栈上第一个未使用的位置(或最后使用的位置,取决于编译器和机器)。框架指针 ( fp ) 指向框架底部附近的位置。所有参数的空间都保留在堆栈中,即使编译器将其中一些参数传递到寄存器中(如果被调用者调用嵌套例程,该例程可能会尝试通过静态链到达词法上周围的参数,则被调用者需要一个标准位置来保存它们)。

Figure 9.2 shows one plausible layout for a stack frame, consistent with Figure 3.1. The stack pointer (sp) points to the first unused location on the stack (or the last used location, depending on the compiler and machine). The frame pointer (fp) points to a location near the bottom of the frame. Space for all arguments is reserved in the stack, even if the compiler passes some of them in registers (the callee will need a standard place to save them if it ever calls a nested routine that may try to reach a lexically surrounding parameter via the static chain).

编号09-02-9780124104099
图 9.2 典型的堆栈框架。虽然我们在页面上将其绘制为向上增长,但在大多数机器上,堆栈实际上是向下向低地址增长的。参数以 fp 的正偏移量访问。局部变量和临时变量以 fp 的负偏移量访问。要传递给被调用例程的参数在框架顶部组装,使用 sp 的正偏移量。

为了维护此堆栈布局,调用序列可能按如下方式操作。调用者

To maintain this stack layout, the calling sequence might operate as follows. The caller

1. 保存所有调用者保存寄存器,这些寄存器的值在调用后可能需要

1. saves any caller-saves registers whose values may be needed after the call

2. 计算参数的值并将其移入堆栈或寄存器

2. computes the values of arguments and moves them into the stack or registers

3. 计算静态链接(如果这是一种具有嵌套子程序的语言),并将其作为额外的隐藏参数传递

3. computes the static link (if this is a language with nested subroutines), and passes it as an extra, hidden argument

4. 使用特殊的子程序调用指令跳转到子程序,同时将返回地址传递到堆栈或寄存器中

4. uses a special subroutine call instruction to jump to the subroutine, simultaneously passing the return address on the stack or in a register

在其序言中,被调用者

In its prologue, the callee

1.通过从 sp中减去一个适当的常数来分配一个框架

1. allocates a frame by subtracting an appropriate constant from the sp

2. 将旧框架指针保存到堆栈中,并将其更新为指向新分配的框架

2. saves the old frame pointer into the stack, and updates it to point to the newly allocated frame

3. 保存任何可能被当前例程覆盖的被调用者保存寄存器(包括静态链接和返回地址,如果它们是在寄存器中传递的)

3. saves any callee-saves registers that may be overwritten by the current routine (including the static link and return address, if they were passed in registers)

子程序完成后,结尾

After the subroutine has completed, the epilogue

1. 将返回值(如果有)移至寄存器或堆栈中的保留位置

1. moves the return value (if any) into a register or a reserved location in the stack

2. 如果需要,恢复被调用者保存的寄存器

2. restores callee-saves registers if needed

3. 恢复fpsp

3. restores the fp and the sp

4. 跳回到返回地址

4. jumps back to the return address

最后,调用者

Finally, the caller

1. 将返回值移动到需要的地方

1. moves the return value to wherever it is needed

2. 如果需要,恢复调用者保存的寄存器图片

2. restores caller-saves registers if needed

特殊情况优化

Special-Case Optimizations

在常见情况下,调用序列、序言和结尾的许多部分都可以省略。如果硬件将返回地址传递到寄存器中,那么叶例程(在返回之前不进行其他调用的子例程)2可以简单地将其留在那里;它不需要将其保存在堆栈中。同样,它不需要保存静态链接或任何调用者保存的寄存器。

Many parts of the calling sequence, prologue, and epilogue can be omitted in common cases. If the hardware passes the return address in a register, then a leaf routine (a subroutine that makes no additional calls before returning)2 can simply leave it there; it does not need to save it in the stack. Likewise it need not save the static link or any caller-saves registers.

没有局部变量且无需保存或恢复的子程序甚至可能不需要 RISC 计算机上的堆栈框架。最简单的子程序(例如,用于计算标准数学函数的库程序)可能根本不接触内存,除非获取指令:它们可能在寄存器中获取参数,完全在(调用者保存)寄存器中计算,不调用其他程序,并在寄存器中返回结果。因此,它们可能非常快。

A subroutine with no local variables and nothing to save or restore may not even need a stack frame on a RISC machine. The simplest subroutines (e.g., library routines to compute the standard mathematical functions) may not touch memory at all, except to fetch instructions: they may take their arguments in registers, compute entirely in (caller-saves) registers, call no other routines, and return their results in registers. As a result they may be extremely fast.

9.2.1 显示

9.2.1 Displays

静态链的一个缺点是,访问k级范围内的对象需要对静态链进行k次取消引用。如果本地对象可以通过一次(位移模式)内存访问加载到寄存器中,则k级对象将需要k + 1 次内存访问。通过使用显示,可以将这个数字减少为一个常数。

One disadvantage of static chains is that access to an object in a scope k levels out requires that the static chain be dereferenced k times. If a local object can be loaded into a register with a single (displacement mode) memory access, an object k levels out will require k + 1 memory accesses. This number can be reduced to a constant by use of a display.

在09-02-9780124104099 更深入地

IN MORE DEPTH

如配套网站所述,显示是一个替代静态链的小数组。显示的第j个元素包含对词汇嵌套级别j上最近活动子例程的框架的引用。如果当前活动例程嵌套深度i > 3 级,则显示的元素i − 1、i − 2 和i − 3 包含静态链的前三个链接的值。可以在显示的元素j = ik中存储的地址的静态已知偏移处找到第k级的对象。

As described on the companion site, a display is a small array that replaces the static chain. The jth element of the display contains a reference to the frame of the most recently active subroutine at lexical nesting level j. If the currently active routine is nested i > 3 levels deep, then elements i − 1, i − 2, and i − 3 of the display contain the values that would have been the first three links of the static chain. An object k levels out can be found at a statically known offset from the address stored in element j = ik of the display.

对于大多数程序来说,在子程序调用序列中维护显示的成本往往略高于维护静态链的成本。同时,现代编译器降低了取消引用静态链的成本,这些编译器往往会在适当的时候很好地将链接缓存在寄存器中。这些观察结果,加上语言(尤其是从 C 派生出来的语言)中子程序不嵌套的趋势,使得今天的显示比 20 世纪 70 年代不那么常见了。

For most programs the cost of maintaining a display in the subroutine calling sequence tends to be slightly higher than that of maintaining a static chain. At the same time, the cost of dereferencing the static chain has been reduced by modern compilers, which tend to do a good job of caching the links in registers when appropriate. These observations, combined with the trend toward languages (those descended from C in particular) in which subroutines do not nest, have made displays less common today than they were in the 1970s.

9.2.2 堆栈案例研究:ARM 上的 LLVM;x86 上的gcc

9.2.2 Stack Case Studies: LLVM on ARM; gcc on x86

调用序列在不同的机器之间,甚至在不同的编译器之间都有很大差异,尽管硬件供应商通常会发布针对各自架构的建议约定,以促进不同编译器生成的程序组件之间的互操作性。许多最显著的差异反映了随着时间的推移,寄存器的使用越来越频繁,内存的使用越来越少。这种演变反映了至少三个重要的技术趋势:寄存器集的大小不断增加,速度差距越来越大寄存器和内存(甚至 L1 缓存),以及编译器和处理器通过重新排序指令来提高性能的能力(至少当操作数都在寄存器中时)。

Calling sequences differ significantly from machine to machine and even compiler to compiler, though hardware vendors typically publish suggested conventions for their respective architectures, to promote interoperability among program components produced by different compilers. Many of the most significant differences reflect an evolution over time toward heavier use of registers and lighter use of memory. This evolution reflects at least three important technological trends: the increasing size of register sets, the increasing gap in speed between registers and memory (even L1 cache), and the increasing ability of both compilers and processors to improve performance by reordering instructions—at least when operands are all in registers.

较旧的编译器(尤其是寄存器数量较少的机器)倾向于在堆栈上传递参数;较新的编译器(尤其是寄存器组较大的机器)倾向于在寄存器中传递参数。较旧的体系结构倾向于提供将返回地址推送到堆栈的子例程调用指令;较新的体系结构倾向于将返回地址放在寄存器中。

Older compilers, particularly for machines with a small number of registers, tend to pass arguments on the stack; newer compilers, particularly for machines with larger register sets, tend to pass arguments in registers. Older architectures tend to provide a subroutine call instruction that pushes the return address onto the stack; newer architectures tend to put the return address in a register.

许多机器提供在子程序调用序列中使用的特殊指令。例如,在 x86 上,enterleave指令通过同时更新帧指针和堆栈指针来分配和释放堆栈帧。在 ARM 上,stm(存储多个)和ldm(加载多个)指令保存和恢复任意寄存器组;在一个常见的习惯用法中,保存的集合包括返回地址(“链接寄存器”);当恢复的集合包括程序计数器(在同一位置)时,ldm可以在一条指令中弹出一组寄存器并从子程序返回。

Many machines provide special instructions of use in the subroutine-call sequence. On the x86, for example, enter and leave instructions allocate and deallocate stack frames, via simultaneous update of the frame pointer and stack pointer. On the ARM, stm (store multiple) and ldm (load multiple) instructions save and restore arbitrary groups of registers; in one common idiom, the saved set includes the return address (“link register”); when the restored set includes the program counter (in the same position), ldm can pop a set of registers and return from the subroutine in a single instruction.

还有一种趋势(虽然不太一致)是不再使用专用的帧指针寄存器。在较旧的编译器中,对于较旧的机器,通常使用pushpop指令来传递基于堆栈的参数。由此导致的 sp 值的不稳定性使得很难(但并非不可能)使用该寄存器作为访问局部变量的基础。单独的帧指针简化了代码生成和符号调试。同时,它在子例程调用序列中引入了额外的指令,并将可用于其他目的的寄存器数量减少了一个。现代编译器编写者越来越愿意牺牲复杂性来换取性能,并且经常放弃使用帧指针,至少在简单的例程中是这样。

There has also been a trend—though a less consistent one—away from the use of a dedicated frame pointer register. In older compilers, for older machines, it was common to use push and pop instructions to pass stack-based arguments. The resulting instability in the value of the sp made it difficult (though not impossible) to use that register as the base for access to local variables. A separate frame pointer simplified both code generation and symbolic debugging. At the same time, it introduced additional instructions into the subroutine calling sequence, and reduced by one the number of registers available for other purposes. Modern compiler writers are increasingly willing to trade complexity for performance, and often dispense with the frame pointer, at least in simple routines.

在09-02-9780124104099 更深入地

IN MORE DEPTH

在配套站点上,我们详细介绍了具有代表性的一对编译器的堆栈布局约定和调用序列:用于 32 位 ARMv7 架构的 LLVM 编译器,以及用于 32 位和 64 位 x86的 gcc编译器。LLVM 是一种中/后端组合,最初由伊利诺伊大学开发,现在广泛应用于学术界和工业界。除其他外,它构成了 iPhone(iOS)和 Android 设备标准工具链的骨干。GNU 编译器集合gcc是开源运动的基石,被广泛应用于各种各样的笔记本电脑、台式机和服务器。LLVM 和gcc都具有适用于许多目标架构的后端和适用于许多编程语言的前端。我们专注于它们对 C 的支持,从某种意义上说,C 的约定是其他语言的“最低公分母”。

On the companion site we look in some detail at the stack layout conventions and calling sequences of a representative pair of compilers: the LLVM compiler for the 32-bit ARMv7 architecture, and the gcc compiler for the 32- and 64-bit x86. LLVM is a middle/back end combination originally developed at the University of Illinois and now used extensively in both academia and industry. Among other things, it forms the backbone of the standard tool chains for both iPhone (iOS) and Android devices. The GNU compiler collection, gcc, is a cornerstone of the open source movement, used across a huge variety of laptops, desktops, and servers. Both LLVM and gcc have back ends for many target architectures, and front ends for many programming languages. We focus on their support for C, whose conventions are in some sense a “lowest common denominator” for other languages.

9.2.3 注册窗口

9.2.3 Register Windows

作为在子程序调用和返回时保存和恢复寄存器的替代方法,最初的 Berkeley RISC 计算机 [ PD80Pat85 ] 引入了一种称为寄存器窗口的硬件机制。基本思想是将 ISA 有限的寄存器名称集映射到更大的物理寄存器集合的某个子集(窗口)上,并在进行子程序调用时更改映射。旧映射和新映射略有重叠,允许在交集处传递参数(并返回函数结果)。

As an alternative to saving and restoring registers on subroutine calls and returns, the original Berkeley RISC machines [PD80, Pat85] introduced a hardware mechanism known as register windows. The basic idea is to map the ISA's limited set of register names onto some subset (window) of a much larger collection of physical registers, and to change the mapping when making subroutine calls. Old and new mappings overlap a bit, allowing arguments to be passed (and function results returned) in the intersection.

在09-02-9780124104099 更深入地

IN MORE DEPTH

我们在配套网站上更详细地讨论了寄存器窗口。它们已出现在多种商用处理器中,最著名的是 Sun SPARC 和 Intel IA-64 (Itanium)。

We consider register windows in more detail on the companion site. They have appeared in several commercial processors, most notably the Sun SPARC and the Intel IA-64 (Itanium).

9.2.4 在线扩展

9.2.4 In-Line Expansion

作为基于堆栈的调用约定的替代方案,许多语言实现允许在调用点内联扩展某些子例程。“被调用”例程的副本成为“调用者”的一部分;不会发生实际的子例程调用。内联扩展可避免各种开销,包括空间分配、调用和返回的分支延迟、维护静态链或显示以及(通常)保存和恢复寄存器。它还允许编译器执行代码改进,例如全局寄存器分配、指令调度和跨子例程边界的公共子表达式消除 - 这是大多数编译器无法做到的。

As an alternative to stack-based calling conventions, many language implementations allow certain subroutines to be expanded in-line at the point of call. A copy of the “called” routine becomes a part of the “caller”; no actual subroutine call occurs. In-line expansion avoids a variety of overheads, including space allocation, branch delays from the call and return, maintaining the static chain or display, and (often) saving and restoring registers. It also allows the compiler to perform code improvements such as global register allocation, instruction scheduling, and common subexpression elimination across the boundaries between subroutines—something that most compilers can't do otherwise.

例 9.6

Example 9.6

请求内联子程序

Requesting an inline subroutine

在许多实现中,编译器会选择哪些子例程以内联方式扩展,哪些子例程以常规方式编译。在某些语言中,程序员可以建议将特定例程以内联方式编译。在 C 和 C++ 中,关键字inline可以作为函数声明的前缀:

In many implementations, the compiler chooses which subroutines to expand in-line and which to compile conventionally. In some languages, the programmer can suggest that particular routines be in-lined. In C and C++, the keyword inline can be prefixed to a function declaration:

内联 int max(int a, int b) {返回 a > b ? a : b;}

inline int max(int a, int b) {return a > b ? a : b;}

在 Ada 中,程序员可以使用重要注释指令来请求内联扩展:

In Ada, the programmer can request in-line expansion with a significant comment, or pragma:

函数 max(a, b : 整数) 返回整数是

function max(a, b : integer) return integer is

开始

begin

 如果 a > b 则返回 a;否则返回 b;结束 if;

 if a > b then return a; else return b; end if;

结束最大值;

end max;

pragma inline(最大);

pragma inline(max);

与C 和C++ 的inline类似,该指令是一个提示;编译器可以忽略它。■

Like the inline of C and C++, this pragma is a hint; the compiler is permitted to ignore it. ■

例 9.7

Example 9.7

内联和递归

In-lining and recursion

第 3.7 节中,我们指出了内联扩展和宏之间的相似性,但认为前者在语义上更可取。事实上,内联扩展在语义上是中性的:它纯粹是一种实现技术,对程序的含义没有影响。与真正的子程序调用相比,内联扩展的明显缺点是增加了代码大小,因为子程序的整个主体出现在每个调用站点。在递归子程序的一般情况下,内联扩展也不是一种选择。对于偶尔可能但不太可能进行递归调用的情况,可能需要生成一个真正的递归子程序,但在每个调用站点内联扩展该程序的一个级别。举一个简单的例子,考虑一个二叉树,其叶子包含字符串。在 C++ 中,返回这棵树的边缘(其叶子中值的从左到右的连接)的例程可能看起来像这样:

In Section 3.7 we noted the similarity between in-line expansion and macros, but argued that the former is semantically preferable. In fact, in-line expansion is semantically neutral: it is purely an implementation technique, with no effect on the meaning of the program. In comparison to real subroutine calls, in-line expansion has the obvious disadvantage of increasing code size, since the entire body of the subroutine appears at every call site. In-line expansion is also not an option in the general case for recursive subroutines. For the occasional case in which a recursive call is possible but unlikely, it may be desirable to generate a true recursive subroutine, but to expand one level of that routine in-line at each call site. As a simple example, consider a binary tree whose leaves contain character strings. A routine to return the fringe of this tree (the left-to-right concatenation of the values in its leaves) might look like this in C++:

字符串边缘(bin_tree * t){

string fringe(bin_tree *t) {

 // 假设两个子项都为 nil,或者都不是

 // assume both children are nil or neither is

 如果(t->left == 0)返回t->val;

 if (t->left == 0) return t->val;

 返回边缘(t->左)+边缘(t->右);

 return fringe(t->left) + fringe(t->right);

}

}

如果编译器使每个嵌套调用都成为真正的子程序调用,则可以内联扩展此代码。由于二叉树中一半的节点是叶子,因此这种扩展将在运行时消除一半的动态调用。如果我们不仅扩展根调用,而且是真实子程序版本中的两个调用(一个级别的),则仅会保留原始动态调用的四分之一。■

A compiler can expand this code in-line if it makes each nested invocation a true subroutine call. Since half the nodes in a binary tree are leaves, this expansion will eliminate half the dynamic calls at run time. If we expand not only the root calls but also (one level of) the two calls within the true subroutine version, only a quarter of the original dynamic calls will remain. ■

设计与实现

Design & Implementation

9.1 提示和指示

9.1 Hints and directives

C 和 C++ 中的inline关键字建议但不要求编译器以内联方式扩展子程序。如果编译器有理由相信这样会产生更好的代码,则在指定了inline时可以使用常规实现,在未指定inline时可以使用内联实现。(在这两种语言中, inline关键字也会影响单独编译的规则。特别是,为了方便将它们包含在头文件中,内联函数可以有多个定义。C++ 规定所有定义必须相同;而在 C 中,它们之间的选择是明确未指定的。)

The inline keyword in C and C++ suggests but does not require that the compiler expand the subroutine in-line. A conventional implementation may be used when inline has been specified—or an in-line implementation when inline has not been specified—if the compiler has reason to believe that this will result in better code. (In both languages, the inline keyword also has an impact on the rules regarding separate compilation. In particular, to facilitate their inclusion in header files, inline functions are allowed to have multiple definitions. C++ says all the definitions must be the same; in C, the choice among them is explicitly unspecified.)

实际上,在编程语言中加入内联之类的提示代表着承认专家程序员的建议有时可能对当前的编译器技术有用,但这种情况将来可能会改变。相比之下,如边栏 8.8 中讨论的那样,使用指针算法代替数组下标更像是一种指示,而不是提示,可能会使从遗留程序生成高质量代码变得复杂。

In effect, the inclusion of hints like inline in a programming language represents an acknowledgment that advice from the expert programmer may sometimes be useful with current compiler technology, but that this may change in the future. By contrast, the use of pointer arithmetic in place of array subscripts, as discussed in Sidebar 8.8, is more of a directive than a hint, and may complicate the generation of high-quality code from legacy programs.

在09-01-9780124104099检查你的理解

Check Your Understanding

1. 什么是子程序调用序列?它起什么作用?子程序的序言结尾是什么意思?

1. What is a subroutine calling sequence? What does it do? What is meant by the subroutine prologue and epilogue?

2. 在旧指令集(CISC)和新指令集(RISC)中,调用序列通常有何不同?

2. How do calling sequences typically differ in older (CISC) and newer (RISC) instruction sets?

3. 描述如何在子程序调用期间维护静态链。

3. Describe how to maintain the static chain during a subroutine call.

4. 什么是显示?它与静态链有何不同?

4. What is a display? How does it differ from a static chain?

5. 堆栈指针帧指针寄存器的用途是什么?为什么子程序通常需要两者?

5. What are the purposes of the stack pointer and frame pointer registers? Why does a subroutine often need both?

6. 为什么现代机器通常在寄存器中而不是在堆栈中传递子程序参数?

6. Why do modern machines typically pass subroutine parameters in registers rather than on the stack?

7. 为什么子程序调用约定通常让调用者负责保存一半寄存器,而让被调用者负责保存另一半?

7. Why do subroutine calling conventions often give the caller responsibility for saving half the registers and the callee responsibility for saving the other half?

8. 如果工作可以在调用者或被调用者中完成,为什么我们通常更喜欢在被调用者中完成?

8. If work can be done in either the caller or the callee, why do we typically prefer to do it in the callee?

9. 为什么编译器一般在堆栈中为参数分配空间,即使它们通过寄存器传递参数?

9. Why do compilers typically allocate space for arguments in the stack, even when they pass them in registers?

10.列出在重要的特殊情况下(例如 叶例程可以对子程序调用序列进行的优化。

10. List the optimizations that can be made to the subroutine calling sequence in important special cases (e.g., leaf routines).

11. 内联子程序有何不同?

11. How does an in-line subroutine differ from a macro?

12. 在什么情况下需要以内联方式扩展子程序?

12. Under what circumstances is it desirable to expand a subroutine in-line?

设计与实现

Design & Implementation

9.2 内联和模块化

9.2 In-lining and modularity

内联扩展最重要的论据可能是它允许程序员采用非常模块化的编程风格,使用大量微小的子程序,而不会牺牲性能。这种模块化编程风格对于面向对象语言至关重要,我们将在第10 章中看到。内联的好处在一定程度上被削弱了,因为更改内联函数的定义会强制重新编译该函数的每个用户;更改普通函数的定义(不更改其接口)只会强制重新链接。在具有即时编译的系统中,可以同时实现两全其美(第16.2.1 节)。

Probably the most important argument for in-line expansion is that it allows programmers to adopt a very modular programming style, with lots of tiny subroutines, without sacrificing performance. This modular programming style is essential for object-oriented languages, as we shall see in Chapter 10. The benefit of in-lining is undermined to some degree by the fact that changing the definition of an in-lined function forces the recompilation of every user of the function; changing the definition of an ordinary function (without changing its interface) forces relinking only. The best of both worlds maybe achieved in systems with just-in-time compilation (Section 16.2.1).

9.3 参数传递

9.3 Parameter Passing

大多数子程序都是参数化的:它们接受控制其行为某些方面或指定它们要操作的数据的参数。出现在子程序声明中的参数名称称为形式参数。在特定调用中传递给子程序的变量和表达式称为实际参数。我们一直将实际参数称为参数。在以下两小节中,我们将讨论最常见的参数传递模式,其中大多数是通过传递值、引用或闭包来实现的。在9.3.3 节中,我们将介绍其他机制,包括默认(可选)参数、命名参数和可变长度参数列表。最后,在9.3.4 节中,我们将考虑从函数返回值的机制。

Most subroutines are parameterized: they take arguments that control certain aspects of their behavior, or specify the data on which they are to operate. Parameter names that appear in the declaration of a subroutine are known as formal parameters. Variables and expressions that are passed to a subroutine in a particular call are known as actual parameters. We have been referring to actual parameters as arguments. In the following two subsections, we discuss the most common parameter-passing modes, most of which are implemented by passing values, references, or closures. In Section 9.3.3 we will look at additional mechanisms, including default (optional) parameters, named parameters, and variable-length argument lists. Finally, in Section 9.3.4 we will consider mechanisms for returning values from functions.

例 9.8

Example 9.8

中缀运算符

Infix operators

正如我们在第 6.1 节中提到的,大多数语言使用前缀表示法来调用用户定义的子程序,子程序名称后跟带括号的参数列表。Lisp 将函数名称放在括号内,例如(max ab)。ML 完全省去了括号,除非需要消除歧义:max ab。ML还允许程序员指定某些名称代表中缀运算符,这些名称出现在一对参数之间。在标准 ML 中,甚至可以指定它们的优先级:

As we noted in Section 6.1, most languages use a prefix notation for calls to user-defined subroutines, with the subroutine name followed by a parenthesized argument list. Lisp places the function name inside the parentheses, as in (max a b). ML dispenses with the parentheses entirely, except when needed for disambiguation: max a b. ML also allows the programmer to specify that certain names represent infix operators, which appear between a pair of arguments. In Standard ML one can even specify their precedence:

infixr 8 tothe;      (* 指数 *)

infixr 8 tothe;      (* exponentiation *)

乐趣 x 到 0 = 1.0

fun x tothe 0 = 1.0

  | x 到 n = x * (x 到 (n-1));       (* 假设 n >= 0 *)

  | x tothe n = x * (x tothe(n-1));      (* assume n >= 0 *)

infixr声明表明tothe将是右结合的二元中缀运算符,优先级为 8(乘法和除法为 7,加法和减法为 6)。Fortran 90 还允许程序员定义新的中缀运算符,但要求它们的名称用句点括起来(例如,A .cross. B),并赋予它们相同的优先级。Smalltalk 对其所有操作都使用中缀(或“mixfix”)表示法(无优先级)。■

The infixr declaration indicates that tothe will be a right-associative binary infix operator, at precedence level 8 (multiplication and division are at level 7, addition and subtraction at level 6). Fortran 90 also allows the programmer to define new infix operators, but it requires their names to be bracketed with periods (e.g., A .cross. B), and it gives them all the same precedence. Smalltalk uses infix (or “mixfix”) notation (without precedence) for all its operations. ■

例 9.9

Example 9.9

Lisp 和 Smalltalk 中的控制抽象

Control abstraction in Lisp and Smalltalk

Lisp 和 Smalltalk 语法的一致性使得控制抽象特别有效:用户定义的子程序(Lisp 中的函数,Smalltalk 中的“消息”)使用与内置操作相同的语法样式。例如,考虑if…then…else

The uniformity of Lisp and Smalltalk syntax makes control abstraction particularly effective: user-defined subroutines (functions in Lisp, “messages” in Smalltalk) use the same style of syntax as built-in operations. As an example, consider if… then … else:

如果 a > b 则 max := a; 否则 max := b; 结束 if;-- 艾达
(如果(> ab)(setf 最大 a)(setf 最大 b));Lisp
(a > b) ifTrue: [max <- a] ifFalse: [max <- b]。“闲聊”

在 Ada 中(与大多数命令式语言一样),很明显if…then…else是一个内置语言结构:它看起来不像子程序调用。在 Lisp 和另一方面,在 Smalltalk 中,类似的条件结构在语法上与用户定义的操作没有区别。它们实际上是根据更简单的概念定义的,而不是内置的,尽管它们需要一种特殊的机制来按正常顺序(而不是应用顺序)评估它们的参数(第 6.6.2 节)。■

In Ada (as in most imperative languages) it is clear that if… then … else is a built-in language construct: it does not look like a subroutine call. In Lisp and Smalltalk, on the other hand, the analogous conditional constructs are syntactically indistinguishable from user-defined operations. They are in fact defined in terms of simpler concepts, rather than being built in, though they require a special mechanism to evaluate their arguments in normal, rather than applicative, order (Section 6.6.2). ■

9.3.1 参数模式

9.3.1 Parameter Modes

到目前为止,在对子程序的讨论中,我们忽略了控制参数传递以及确定实际参数和形式参数之间关系的语义规则。一些语言(包括 C、Fortran、ML 和 Lisp)定义了一组适用于所有参数的规则。其他语言(包括 Ada、C++ 和 Swift)提供了两组或更多组规则,分别对应于不同的参数传递模式。与语言设计的许多方面一样,语义细节在很大程度上受到实现问题的影响。

In our discussion of subroutines so far, we have glossed over the semantic rules that govern parameter passing, and that determine the relationship between actual and formal parameters. Some languages, including C, Fortran, ML, and Lisp, define a single set of rules, which apply to all parameters. Other languages, including Ada, C++, and Swift, provide two or more sets of rules, corresponding to different parameter-passing modes. As in many aspects of language design, the semantic details are heavily influenced by implementation issues.

例 9.10

Example 9.10

将参数传递给子程序

Passing an argument to a subroutine

暂时假设x是具有变量值模型的语言中的全局变量,并且我们希望将x作为参数传递给子程序p

Suppose for the moment that x is a global variable in a language with a value model of variables, and that we wish to pass x as a parameter to subroutine p:

p(x);

p(x);

从实现的角度来看,我们有两种主要选择:我们可以为p提供x的值的副本,或者我们可以为其提供x的地址。两种最常见的参数传递模式,称为按值调用按引用调用,旨在反映这些实现。■

From an implementation point of view, we have two principal alternatives: we may provide p with a copy of x's value, or we may provide it with x's address. The two most common parameter-passing modes, called call by value and call by reference, are designed to reflect these implementations. ■

对于值参数,在调用子程序时,每个实际参数都会被赋值给相应的形式参数;从此以后,两者是独立的。对于引用参数,每个形式参数都会在子程序主体中为相应的实际参数引入一个新名称。如果实际参数在子程序中以其原始名称可见(如果它在周围范围内声明,通常就是这种情况),则这两个名称是同一对象的别名,通过一个名称所做的更改将通过另一个名称可见。在大多数语言中(Fortran 是个例外;见下文),要通过引用传递的实际参数必须是左值;它不能是算术运算的结果,也不能是任何其他没有地址的值。

With value parameters, each actual parameter is assigned into the corresponding formal parameter when a subroutine is called; from then on, the two are independent. With reference parameters, each formal parameter introduces, within the body of the subroutine, a new name for the corresponding actual parameter. If the actual parameter is also visible within the subroutine under its original name (as will generally be the case if it is declared in a surrounding scope), then the two names are aliases for the same object, and changes made through one will be visible through the other. In most languages (Fortran is an exception; see below) an actual parameter that is to be passed by reference must be an l-value; it cannot be the result of an arithmetic operation, or any other value without an address.

例 9.11

Example 9.11

值及参考参数

Value and reference parameters

作为一个简单的例子,考虑以下伪代码:

As a simple example, consider the following pseudocode:

x :整数       ——全局

x : integer       –– global

过程 foo(y:整数)

procedure foo(y : integer)

  y := 3

  y := 3

  打印 x

  print x

x := 2

x := 2

foo(x)

foo(x)

打印 x

print x

如果y以值的形式传递给 foo,则foo中的赋值没有任何可见效果 — y对子例程来说是私有的 — 程序会打印两次2。如果y以引用的形式传递给foo,则 foo 中的赋值会改变xy只是x的一个本地名称— 程序会打印两次3。

If y is passed to foo by value, then the assignment inside foo has no visible effect—y is private to the subroutine—and the program prints 2 twice. If y is passed to foo by reference, then the assignment inside foo changes xy is just a local name for x—and the program prints 3 twice. ■

值和参考参数的变化

Variations on Value and Reference Parameters

例 9.12

Example 9.12

按值/结果调用

Call by value/result

如果通过引用调用的目的是允许被调用的例程修改实际参数,我们可以使用通过值/结果调用实现类似的效果,这是在 Algol W 中首次引入的模式。与通过值调用一样,通过值/结果调用在子例程执行开始时将实际参数复制到形式参数中。与通过值调用不同的是,当子例程返回时,它还会将形式参数复制回实际参数中。在示例 9.11中,值/结果会在foo开始时将x复制到y中,并在foo结束时将y复制到x中。由于foo在其间直接访问x,因此程序的可见行为与通过引用调用不同:将3赋值给y直到内部print语句之后才会影响x,因此程序将打印2,然后打印3。

If the purpose of call by reference is to allow the called routine to modify the actual parameter, we can achieve a similar effect using call by value/result, a mode first introduced in Algol W. Like call by value, call by value/result copies the actual parameter into the formal parameter at the beginning of subroutine execution. Unlike call by value, it also copies the formal parameter back into the actual parameter when the subroutine returns. In Example 9.11, value/result would copy x into y at the beginning of foo, and y into x at the end of foo. Because foo accesses x directly in between, the program's visible behavior would be different than it was with call by reference: the assignment of 3 into y would not affect x until after the inner print statement, so the program would print 2 and then 3. ■

例 9.13

Example 9.13

在 C 中模拟引用调用

Emulating call-by-reference in C

在 Pascal 中,参数默认按值传递;如果在子程序头的形式参数列表中,参数前面有关键字var,则按引用传递。C 中的参数始终按值传递,但数组的效果并不常见:由于 C 中数组和指针的互操作性(第 8.5.1 节),按值传递的是指针;通过此指针访问的数组元素的更改对调用者是可见的。要允许被调用的例程修改调用者范围内的数组以外的变量,C 程序员必须明确将指针传递给该变量:

In Pascal, parameters were passed by value by default; they were passed by reference if preceded by the keyword var in their subroutine header's formal parameter list. Parameters in C are always passed by value, though the effect for arrays is unusual: because of the interoperability of arrays and pointers in C (Section 8.5.1), what is passed by value is a pointer; changes to array elements accessed through this pointer are visible to the caller. To allow a called routine to modify a variable other than an array in the caller's scope, the C programmer must pass a pointer to the variable explicitly:

void swap(int *a,int *b) { int t = *a; *a = *b; *b = t; }

void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }

交换(&v1,&v2); 图片

swap(&v1, &v2);

Fortran 通过引用传递所有参数,但不要求每个实际参数都是左值。如果内置表达式出现在参数列表中,则编译器会创建一个临时变量来保存该值,并通过引用传递此变量。如果 Fortran 子例程需要修改其形式参数的值而不修改其实际参数,则必须将这些值复制到局部变量中,然后修改这些变量。

Fortran passes all parameters by reference, but does not require that every actual parameter be an l-value. If a built-up expression appears in an argument list, the compiler creates a temporary variable to hold the value, and passes this variable by reference. A Fortran subroutine that needs to modify the values of its formal parameters without modifying its actual parameters must copy the values into local variables, and modify those instead.

设计与实现

Design & Implementation

9.3 参数模式

9.3 Parameter modes

虽然从实现角度来看引入参数模式(语义问题)似乎有些奇怪,但值参数和引用参数之间的区别从根本上来说是一个实现问题。大多数具有多种模式的语言(Ada 和 Swift 是明显的例外)可能被合理地描述为试图将可接受的语义粘贴到所需的实现上,而不是找到所需语义的可接受实现。

While it may seem odd to introduce parameter modes (a semantic issue) in terms of implementation, the distinction between value and reference parameters is fundamentally an implementation issue. Most languages with more than one mode (Ada and Swift are notable exceptions) might fairly be characterized as an attempt to paste acceptable semantics onto the desired implementation, rather than to find an acceptable implementation of the desired semantics.

通过共享呼叫

按值调用和按引用调用在具有变量值模型的语言中最有意义:它们决定我们是复制变量还是传递它的别名。在 Smalltalk、Lisp、ML 或 Ruby 等语言中,这两个选项都没有意义,因为在这些语言中变量已经是一个引用。在这里,最自然的方法是简单地传递引用本身,并让实际参数和形式参数引用同一个对象。Clu 将这种模式称为共享调用。它不同于按值调用,因为虽然我们确实将实际参数复制到形式参数中,但它们都是引用;如果我们修改形式参数引用的对象,则在子例程返回后,程序将能够通过实际参数看到这些更改。共享调用也不同于按引用调用,因为虽然被调用的例程可以更改实际参数引用的对象的值,但它不能让参数引用不同的对象。

Call by value and call by reference make the most sense in a language with a value model of variables: they determine whether we copy the variable or pass an alias for it. Neither option really makes sense in a language like Smalltalk, Lisp, ML, or Ruby, in which a variable is already a reference. Here it is most natural simply to pass the reference itself, and let the actual and formal parameters refer to the same object. Clu called this mode call by sharing. It is different from call by value because, although we do copy the actual parameter into the formal parameter, both of them are references; if we modify the object to which the formal parameter refers, the program will be able to see those changes through the actual parameter after the subroutine returns. Call by sharing is also different from call by reference, because although the called routine can change the value of the object to which the actual parameter refers, it cannot make the argument refer to a different object.

正如我们在6.1.28.5.1节中提到的,变量的引用模型不一定要求每个对象都通过地址间接访问:实现可以创建不可变对象(数字、字符等)的多个副本并直接访问它们。因此,对于不可变类型的小对象,共享调用通常与值调用相同。

As we noted in Sections 6.1.2 and 8.5.1, a reference model of variables does not necessarily require that every object be accessed indirectly by address: the implementation can create multiple copies of immutable objects (numbers, characters, etc.) and access them directly. Call by sharing is thus commonly implemented the same as call by value for small objects of immutable type.

为了保持变量的混合模型,Java 对原始内置类型(所有类型都是值)的变量使用按值调用,对用户定义类类型(所有类型都是引用)的变量使用共享调用。一个有趣的结果是,Java 子例程无法更改原始类型的实际参数的值。C# 中默认采用类似的方法,但由于该语言允许用户创建值(结构)和引用()类型,因此这两种情况都被视为按值调用。也就是说,无论变量是值还是引用,我们总是通过复制来传递它。(有些作者以同样的方式描述 Java。)

In keeping with its hybrid model of variables, Java uses call by value for variables of primitive, built-in types (all of which are values), and call by sharing for variables of user-defined class types (all of which are references). An interesting consequence is that a Java subroutine cannot change the value of an actual parameter of primitive type. A similar approach is the default in C#, but because the language allows users to create both value (struct) and reference (class) types, both cases are considered call by value. That is, whether a variable is a value or a reference, we always pass it by copying. (Some authors describe Java the same way.)

如果需要,C# 中的参数可以通过引用传递,方法是用refout关键字标记形式参数和每个相应的参数。 这两种模式都是通过传递地址来实现的;它们的不同之处在于,ref参数必须在调用之前明确赋值,如第 6.1.3 节所述;而out参数则不需要。 因此,与 Java 相反,如果参数通过refout传递,C# 子程序可以更改原始类型的实际参数的值。 类似地,如果(引用)类型的变量作为refout参数传递,它最终可能会在子程序执行后引用不同的对象 —— 这是通过共享调用无法实现的。

When desired, parameters in C# can be passed by reference instead, by labeling both a formal parameter and each corresponding argument with the ref or out keyword. Both of these modes are implemented by passing an address; they differ in that a ref argument must be definitely assigned prior to the call, as described in Section 6.1.3; an out argument need not. In contrast to Java, therefore, a C# subroutine can change the value of an actual parameter of primitive type, if the parameter is passed ref or out. Similarly, if a variable of class (reference) type is passed as a ref or out parameter, it may end up referring to a different object as a result of subroutine execution—something that is not possible with call by sharing.

引用调用的目的

从历史上看,在同时提供值参数和引用参数的语言(例如 Pascal 或 Modula)中,程序员在选择值参数和引用参数时可能会考虑两个主要问题。首先,如果被调用的例程应该更改实际参数(参数)的值,那么程序员必须通过引用传递该参数。相反,为了确保被调用的例程不能修改参数,程序员可以通过值传递参数。其次,值参数的实现会将实际值复制到形式值,当参数很大时,这可能是一个耗时的操作。引用参数可以通过传递地址来实现。(当然,访问通过引用传递的参数需要额外的间接层。如果参数使用得足够频繁,这种间接的成本可能超过复制参数的成本。)

Historically, there were two principal issues that a programmer might consider when choosing between value and reference parameters in a language (e.g., Pascal or Modula) that provided both. First, if the called routine was supposed to change the value of an actual parameter (argument), then the programmer had to pass the parameter by reference. Conversely, to ensure that the called routine could not modify the argument, the programmer could pass the parameter by value. Second, the implementation of value parameters would copy actuals to formals, a potentially time-consuming operation when arguments were large. Reference parameters can be implemented simply by passing an address. (Of course, accessing a parameter that is passed by reference requires an extra level of indirection. If the parameter were used often enough, the cost of this indirection might outweigh the cost of copying the argument.)

大值参数的潜在低效率可能会促使程序员通过引用传递参数,而从语义上讲,通过值传递更为合适。例如,Pascal 程序员通常被教导对需要修改的参数和非常大的参数使用var(引用)参数。类似地,当今的 C 程序员通常被教导对需要修改的参数和非常大的参数传递指针(用&创建)。不幸的是,后一种理由往往会导致有缺陷的代码,其中子例程修改了调用者本想保持不变的参数。

The potential inefficiency of large value parameters may prompt programmers to pass an argument by reference when passing by value would be semantically more appropriate. Pascal programmers, for example, were commonly taught to use var (reference) parameters both for arguments that need to be modified and for arguments that are very large. In a similar vein, C programmers today are commonly taught to pass pointers (created with &) for both to-be-modified and very large arguments. Unfortunately, the latter justification tends to lead to buggy code, in which a subroutine modifies an argument that the caller meant to leave unchanged.

只读参数

为了将引用参数的效率与值参数的安全性结合起来,Modula-3 提供了一种READONLY参数模式。任何以READONLY开头的形式参数都不能被调用的例程更改:编译器阻止程序员在任何赋值语句的左侧使用该形式参数、从文件中读取它或通过引用将其传递给任何其他子例程。小的READONLY参数通常通过传递值来实现;较大的READONLY参数通过传递地址来实现。与 Fortran 一样,Modula-3 编译器将创建一个临时变量来保存作为大型READONLY参数传递的任何构建表达式的值。

To combine the efficiency of reference parameters and the safety of value parameters, Modula-3 provided a READONLY parameter mode. Any formal parameter whose declaration was preceded by READONLY could not be changed by the called routine: the compiler prevented the programmer from using that formal parameter on the left-hand side of any assignment statement, reading it from a file, or passing it by reference to any other subroutine. Small READONLY parameters were generally implemented by passing a value; larger READONLY parameters were implemented by passing an address. As in Fortran, a Modula-3 compiler would create a temporary variable to hold the value of any built-up expression passed as a large READONLY parameter.

例 9.14

Example 9.14

C 中的const参数

const parameters in C

C 中也有与READONLY参数等效的功能,它允许在任何变量或参数声明前加上关键字const。Const变量是“阐述时常量”,如第 3.2 节所述。在将指针传递给大型结构时, Const参数特别有用

The equivalent of READONLY parameters is also available in C, which allows any variable or parameter declaration to be preceded by the keyword const. Const variables are “elaboration-time constants,” as described in Section 3.2. Const parameters are particularly useful when passing pointers to large structures:

void append_to_log(const huge_record* r) { …

void append_to_log(const huge_record* r) { …

追加到日志(&我的记录);

append_to_log(&my_record);

这里关键字const适用于r指向的记录;3被调用者将无法更改记录的内容。但请注意,在 C 中,调用者必须明确创建指向记录的指针,并且编译器没有按值传递的选项。■

Here the keyword const applies to the record to which r points;3 the callee will be unable to change the record's contents. Note, however, that in C the caller must create a pointer to the record explicitly, and the compiler does not have the option of passing by value. ■

参数模式(尤其是READONLY模式)的一个传统问题是,它们容易混淆关键的实际问题(实现传递的是值还是引用?)和两个语义问题:被调用者是否被允许更改形式参数,如果可以,更改是否会反映在实际参数中?C 通过强制程序员使用指针显式传递引用,将实用问题分开。然而,它的const模式有双重作用: const foo* p的目的是为了保护实际参数不被更改,还是为了记录子例程将形式参数视为常量而不是变量的事实,还是两者兼而有之?

One traditional problem with parameter modes—and with the READONLY mode in particular—is that they tend to confuse the key pragmatic issue (does the implementation pass a value or a reference?) with two semantic issues: is the callee allowed to change the formal parameter and, if so, will the changes be reflected in the actual parameter? C keeps the pragmatic issue separate, by forcing the programmer to pass references explicitly with pointers. Still, its const mode serves double duty: is the intent of const foo* p to protect the actual parameter from change, or to document the fact that the subroutine thinks of the formal parameter as a constant rather than a variable, or both?

Ada 中的参数模式

Parameter Modes in Ada

Ada 提供三种参数传递模式,分别称为inoutin out。输入参数将信息从调用者传递给被调用者;它们可以被调用者读取但不能写入。输出参数将信息从被调用者传递给调用者。在 Ada 83 中,它们可以被调用者写入但不能读取;在 Ada 95 中,它们既可以读取也可以写入,但它们的生命周期开始时未初始化。输入输出参数双向传递信息;它们既可以读取也可以写入。对输出输入输出参数的更改始终会更改实际参数。

Ada provides three parameter-passing modes, called in, out, and in out. In parameters pass information from the caller to the callee; they can be read by the callee but not written. Out parameters pass information from the callee to the caller. In Ada 83 they can be written by the callee but not read; in Ada 95 they can be both read and written, but they begin their life uninitialized. In out parameters pass information in both directions; they can be both read and written. Changes to out or in out parameters always change the actual parameter.

对于标量和访问(指针)类型的参数,Ada 规定所有三种模式都应通过复制值来实现。因此,对于这些参数,in是按值调用,in out是按值/结果调用,out只是按结果调用(子程序返回时,形式参数的值被复制到实际参数中)。但是,对于大多数构造类型的参数,Ada 明确允许实现传递值或引用。在大多数语言中,这两种不同的机制会导致不同的语义:对按引用传递的in out参数所做的更改将立即影响实际参数;对按值传递的in out参数所做的更改直到子程序返回才会影响实际参数。如示例 9.12中所述,在存在别名的情况下,这种差异可能导致不同的行为。

For parameters of scalar and access (pointer) types, Ada specifies that all three modes are to be implemented by copying values. For these parameters, then, in is call by value, in out is call by value/result, and out is simply call by result (the value of the formal parameter is copied into the actual parameter when the subroutine returns). For parameters of most constructed types, however, Ada specifically permits an implementation to pass either values or references. In most languages, these two different mechanisms would lead to different semantics: changes made to an in out parameter that is passed by reference will affect the actual parameter immediately; changes made to an in out parameter that is passed by value will not affect the actual parameter until the subroutine returns. As noted in Example 9.12, the difference can lead to different behavior in the presence ofaliases.

隐藏引用和值/结果之间的区别的一种可能方法是禁止创建别名,就像 Euclid 所做的那样。Ada 采取了一种更简单的策略:如果程序能够区分 out参数中基于值和引用的实现(非标量、非指针),则该程序被认为是错误的— 虽然不正确,但语言实现不需要捕捉到。

One possible way to hide the distinction between reference and value/result would be to outlaw the creation of aliases, as Euclid does. Ada takes a simpler tack: a program that can tell the difference between value and reference-based implementations of (nonscalar, nonpointer) in out parameters is said to be erroneous—incorrect, but in a way that the language implementation is not required to catch.

Ada 的参数传递语义允许使用一组模式,不仅用于子程序参数,还用于并发执行任务之间的通信(将在第 13 章中讨论)。当任务在单独的机器上执行且没有共同的内存时,传递实际参数的地址不是一个实用的选择。大多数 Ada 编译器通过引用将大参数传递给子程序;它们通过复制将它们传递给任务的入口点。

Ada's semantics for parameter passing allow a single set of modes to be used not only for subroutine parameters but also for communication among concurrently executing tasks (to be discussed in Chapter 13). When tasks are executing on separate machines, with no memory in common, passing the address of an actual parameter is not a practical option. Most Ada compilers pass large arguments to subroutines by reference; they pass them to the entry points of tasks by copying.

C++ 中的引用

References in C++

例 9.15

Example 9.15

C++ 中的引用参数

Reference parameters in C++

在使用其他语言有一定经验后转而使用 C 的程序员经常会因为 C 缺乏引用参数而感到沮丧。如上所述,人们总是可以通过传递指针来修改对象,但形式参数声明为指针,每次使用时都必须明确取消引用。C++ 通过引入显式的引用概念来解决这个问题。引用参数通过在函数头中的名称前加上与符号来指定:

Programmers who switch to C after some experience with other languages are often frustrated by C's lack of reference parameters. As noted above, one can always arrange to modify an object by passing a pointer, but then the formal parameter is declared as a pointer, and must be explicitly dereferenced whenever it is used. C++ addresses this problem by introducing an explicit notion of a reference. Reference parameters are specified by preceding their name with an ampersand in the header of the function:

void swap(int &a, int &b) { int t = a; a = b; b = t; }

void swap(int &a, int &b) { int t = a; a = b; b = t; }

在此交换例程的代码中, abint,而不是指向int的指针;无需取消引用。此外,调用者将要交换值的变量作为参数传递,而不是传递指向它们的指针。■

In the code of this swap routine, a and b are ints, not pointers to ints; no dereferencing is required. Moreover, the caller passes as arguments the variables whose values are to be swapped, rather than passing pointers to them. ■

与 C 一样,C++ 参数可以声明为const,以确保它不会被修改。对于大型类型, C++ 中的const引用参数提供与 Modula-3 的READONLY参数相同的速度和安全性组合:它们可以通过地址传递,并且不能被调用的例程更改。

As in C, a C++ parameter can be declared to be const to ensure that it is not modified. For large types, const reference parameters in C++ provide the same combination of speed and safety found in the READONLY parameters of Modula-3: they can be passed by address, and cannot be changed by the called routine.

例 9.16

Example 9.16

C++ 中的引用作为别名

References as aliases in C++

C++ 中的引用主要用作参数,但它们也可以出现在其他上下文中。任何变量都可以声明为引用:

References in C++ see their principal use as parameters, but they can appear in other contexts as well. Any variable can be declared to be a reference:

int 我;

int i;

int &j = i;

int &j = i;

我=2;

i = 2;

j=3;

j = 3;

cout << i;      // 打印 3

cout << i;     // prints 3

这里j是i的引用(别名)。声明中的初始化器是必需的;它标识了j是其别名的对象。此外,以后不可能更改j所引用的对象;它将始终引用i

Here j is a reference to (an alias for) i. The initializer in the declaration is required; it identifies the object for which j is an alias. Moreover it is not possible later to change the object to which j refers; it will always refer to i.

通过读取另一个,可以看到对ij 的任何更改。大多数 C++ 编译器都使用地址实现引用。在此示例中, i将被分配一个包含整数的位置,而j将被分配一个包含i地址的位置。尽管它们的实现不同,但ij之间没有语义差异;可以对它们应用完全相同的操作,并得到完全相同的结果。■

Any change to i or j can be seen by reading the other. Most C++ compilers implement references with addresses. In this example, i will be assigned a location that contains an integer, while j will be assigned a location that contains the address of i. Despite their different implementation, however, there is no semantic difference between i and j; the exact same operations can be applied to either, with precisely the same results. ■

例 9.17

Example 9.17

使用内联别名简化代码

Simplifying code with an in-line alias

在 C 语言中,程序员有时会使用指针来避免重复使用相同的复杂表达式:

In C, programmers sometimes use a pointer to avoid repeated uses of the same complex expression:

{

{

 元素*e = &ruby.chemical_composition.elements[1];

 element* e = &ruby.chemical_composition.elements[1];

 e->名称 = “Al”;

 e->name = “Al”;

 e->原子序数 = 13;

 e->atomic_number = 13;

 e->原子重量 = 26.98154;

 e->atomic_weight = 26.98154;

 e->金属 = 真;

 e->metallic = true;

}

}

引用避免了使用指针语法:

References avoid the need for pointer syntax:

{

{

 元素&e = ruby​​.chemical_composition.elements[1];

 element& e = ruby.chemical_composition.elements[1];

 e.名称 = “Al”;

 e.name = “Al”;

 e.原子序数=13;

 e.atomic_number = 13;

 原子重量=26.98154;

 e.atomic_weight = 26.98154;

 e.金属=真;

 e.metallic = true;

} 图片

}

例 9.18

Example 9.18

从函数返回引用

Returning a reference from a function

然而,除了函数参数之外,C++ 中引用的最重要用途是函数返回。C -8.7 节解释了在 C++ 中引用如何用于 I/O。重载的 << 和 >> 运算符返回对其第一个参数的引用,该引用又可以传递给后续的 << 或 >> 操作。语法

Aside from function parameters, however, the most important use of references in C++ is for function returns. Section C-8.7 explains how references are used for I/O in C++. The overloaded << and >> operators return a reference to their first argument, which can in turn be passed to subsequent << or >> operations. The syntax

输出<<a<<b<<c;

cout << a << b << c;

是缩写

is short for

((cout.操作符<<(a)).操作符<<(b)).操作符<<(c);

((cout.operator<<(a)).operator<<(b)).operator<<(c);

如果没有引用,<< 和 >> 就必须返回指向其流的指针:

Without references, << and >> would have to return a pointer to their stream:

((cout.操作符<<(a))->操作符<<(b))->操作符<<(c);

((cout.operator<<(a))->operator<<(b))->operator<<(c);

或者

or

*(*(cout.操作符<<(a))。操作符<<(b))。操作符<<(c);

*(*(cout.operator<<(a)).operator<<(b)).operator<<(c);

这种改变会破坏运算符形式的级联语法:

This change would spoil the cascading syntax of the operator form:

*(*(cout<<a)<<b)<<c;

*(*(cout << a) << b) << c;

和指针一样,函数返回的引用也为在局部变量范围有限的语言(如 C++)中创建悬空引用提供了机会。在我们的 I/O 示例中,返回值是作为参数传递给运算符 << 的同一个流;由于该流的存在时间比函数调用长,因此继续使用引用是安全的。■

Like pointers, references returned from functions introduce the opportunity to create dangling references in a language (like C++) with limited extent for local variables. In our I/O example, the return value is the same stream that was passed into operator<< as a parameter; since this outlives the function invocation, continued use of the reference is safe. ■

需要注意的是,从函数返回引用的功能在 C++ 中并不是新特性:Algol 68 提供了相同的功能。C++ 的面向对象特性及其运算符重载使引用返回特别有用。

It should be noted that the ability to return references from functions is not new in C++: Algol 68 provides the same capability. The object-oriented features of C++, and its operator overloading, make reference returns particularly useful.

R 值参考

R-value References

例 9.19

Example 9.19

C++11 中的 R 值引用

R-value references in C++11

C++ 的一个独特功能是 C++11 中引入的r 值引用概念。R 值引用允许将通常被视为 r 值的参数(通常是构建的表达式)通过引用传递给函数。要了解这为何有用,请考虑以下声明:

One feature that is distinctive in C++ is the notion of an r-value reference, introduced in C++11. R-value references allow an argument that would normally be considered an r-value—typically, a built-up expression—to be passed to a function by reference. To see why this might be useful, consider the following declaration:

对象o2=o1;

obj o2 = o1;

假设o1也属于类obj,编译器将通过调用obj复制构造函数方法初始化o2 ,并将o1作为参数传递。正如我们将在10.3 节中看到的那样,可以声明一个构造函数来接受像任何其他函数一样的参数。从历史上看,obj的复制构造函数的参数将是一个常量引用(const obj&),构造函数的主体将检查此参数以决定如何初始化o2。到目前为止一切顺利。现在考虑类obj的对象包含指向动态分配状态的指针的情况。(标准库的字符串、向量、列表、树和哈希表都具有这样的动态状态。)如果该状态是可变的,则构造函数通常需要分配和初始化一个副本,以便两个对象都不会因对另一个对象的后续更改而受到损坏。但现在考虑声明

Assuming that o1 is also of class obj, the compiler will initialize o2 by calling obj's copy constructor method, passing o1 as argument. As we shall see in Section 10.3, a constructor can be declared to take parameters like those of any other function. Historically, the parameter of obj's copy constructor would have been a constant reference (const obj&), and the body of the constructor would have inspected this parameter to decide how to initialize o2. So far so good. Now consider the case in which objects of class obj contain pointers to dynamically allocated state. (The strings, vectors, lists, trees, and hash tables of the standard library all have such dynamic state.) If that state is mutable, the constructor will generally need to allocate and initialize a copy, so that neither object will be damaged by subsequent changes to the other. But now consider the declaration

obj o3 = foo(“嗨,妈妈”);

obj o3 = foo(“hi mom”);

假设foo的返回类型为obj,编译器将再次创建对复制构造函数的调用,但这次它可能会传递一个临时对象(称为t)用于保存从foo返回的值。与以前一样,构造函数将分配并初始化t中包含的状态的副本,但在其返回时, t中的副本将被销毁(通过调用其析构函数方法,这可能会释放它在堆中占用的空间)。如果我们可以将t 的状态转移到o3中,而不是创建副本然后立即销毁原始副本,这不是很方便吗?这正是右值引用允许的。

Assuming that foo has return type obj, the compiler will again create a call to the copy constructor, but this time it may pass a temporary object (call it t) used to hold the value returned from foo. As before, the constructor will allocate and initialize a copy of the state contained in t, but upon its return the copy in t will be destroyed (by calling its destructor method, which will presumably free the space it consumes in the heap). Wouldn't it be handy if we could transfer t's state into o3, rather than creating a copy and then immediately destroying the original? This is precisely what r-value references allow.

除了传统的复制构造函数及其const obj&参数之外,C++11 还允许程序员声明移动构造函数,其参数为obj&&(两个 & 符号,无const)。当且仅当声明中的参数是“临时”时(即在计算其出现的表达式后将不再可访问的值),编译器才会使用移动构造函数。在o3的声明中, foo的返回值就是这样一个临时值。如果通过名为payload的字段访问obj对象的动态分配状态,则移动构造函数可能很简单

In addition to the conventional copy constructor, with its const obj& parameter, C++11 allows the programmer to declare a move constructor, with an obj&& parameter (double ampersand, no const). The compiler will use the move constructor when—and only when—the parameter in a declaration is a “temporary”—a value that will no longer be accessible after evaluation of the expression in which it appears. In the declaration of o3, the return value of foo is such a temporary. If the dynamically allocated state of an obj object is accessed through a field named payload, the move constructor might be as simple as

obj::obj(obj&& 其他) {

obj::obj(obj&& other) {

 有效载荷=其他.有效载荷;

 payload = other.payload;

 其他.payload = nullptr;

 other.payload = nullptr;

}

}

将other.payload明确置为null可防止other的析构函数释放已转移的状态。■

The explicit null-ing of other.payload prevents other's destructor from freeing the transferred state. ■

在某些情况下,程序员可能知道在将值作为参数传递后永远不会使用,但编译器可能无法推断出这一事实。为了强制使用移动构造函数,程序员可以将值包装在对标准库移动例程的调用中:

In some cases, the programmer may know that a value will never be used after passing it as a parameter, but the compiler may be unable to deduce this fact. To force the use of a move constructor, the programmer can wrap the value in a call to the standard library move routine:

obj o4 = std::move(o3);

obj o4 = std::move(o3);

移动例程不生成任何代码:它实际上是一种强制转换。如果程序实际上包含对o3的后续使用,则行为未定义。

The move routine generates no code: it is, in effect, a cast. Behavior is undefined if the program actually does contain a subsequent use of o3.

和常规引用一样,右值引用可用于 C++ 中任意变量的声明。实际上,它们很少出现在移动构造函数和类似的移动赋值方法(重载 = 运算符)的参数之外。

Like regular references, r-value references can be used in the declaration of arbitrary variables in C++. In practice, they seldom appear outside the parameters of move constructors and the analogous move assignment methods, which overload the = operator.

闭包作为参数

Closures as Parameters

例 9.20

Example 9.20

Ada 中的子程序作为参数

Subroutines as parameters in Ada

闭包(对子程序的引用,连同其引用环境)可能由于多种原因而作为参数传递。最明显的原因出现在参数被声明为子程序(有时称为正式子程序)时。在 Ada 中,可以这样写

A closure (a reference to a subroutine, together with its referencing environment) may be passed as a parameter for any of several reasons. The most obvious of these arises when the parameter is declared to be a subroutine (sometimes called a formal subroutine). In Ada one might write

1.类型 int_func 是访问函数(n:整数)返回整数;

1. type int_func is access function (n : integer) return integer;

2.类型 int_array 是整数数组(正范围<>);

2. type int_array is array (positive range <>) of integer;

3.程序 apply_to_A (f : int_func; A : in out int_array) 是

3. procedure apply_to_A (f : int_func; A : in out int_array) is

4.开始

4. begin

5.    for i in A'range 循环

5.   for i in A'range loop

6.     A(i) := f(A(i));

6.    A(i) := f(A(i));

7.   结束循环;

7.   end loop;

8.结束apply_to_A;    …

8. end apply_to_A;   …

9.    k : integer := 3;    -- 在嵌套范围内...

   

9.   k : integer := 3;   -- in nested scope

   

10.   函数 add_k (m : integer) 返回整数是

10.   function add_k (m : integer) return integer is

11.   开始

11.   begin

12.   返回m+k;

12.   return m + k;

13.   结束 add_k;

   …

13.   end add_k;

   …

14.    apply_to_A(add_k'access,B);

14.   apply_to_A (add_k'access, B);

如第 3.6.1 节所述,闭包需要包含代码地址和引用环境,因为在具有嵌套子程序的语言中,我们需要确保第 6 行f可用的环境与第 14 行直接调用add_k时可用的环境相同- 特别是,它包含k的绑定。■

As discussed in Section 3.6.1, a closure needs to include both a code address and a referencing environment because, in a language with nested subroutines, we need to make sure that the environment available to f at line 6 is the same that would have been available to add_k if it had been called directly at line 14—in particular, that it includes the binding for k. ■

例 9.21

Example 9.21

Scheme 中的一等子程序

First-class subroutines in Scheme

在函数式语言中,子程序通常作为参数传递(并作为结果返回)。基于列表的apply_to_A版本在 Scheme 中看起来类似这样(有关carcdrcons的含义,请参阅第 8.6 节):

Subroutines are routinely passed as parameters (and returned as results) in functional languages. A list-based version of apply_to_A would look something like this in Scheme (for the meanings of car, cdr, and cons, see Section 8.6):

(定义适用于 L

(define apply-to-L

 (λ(fl)

 (lambda (f l)

  (如果(空?l)'()

  (if (null? l) '()

   (cons (f (car l)) (apply-to-L f (cdr l))))))

   (cons (f (car l)) (apply-to-L f (cdr l))))))

因为 Scheme 是动态类型的,所以不需要指定f的类型。在运行时,如果f不是函数,Scheme 实现将在(f (car l))中宣布动态语义错误;如果l不是列表,则在(null? l)(car l)(cdr l)中宣布动态语义错误。■

Since Scheme is dynamically typed, there is no need to specify the type of f. At run time, a Scheme implementation will announce a dynamic semantic error in (f (car l)) if f is not a function, and in (null? l), (car l),or (cdr l) if l is not a list. ■

例 9.22

Example 9.22

OCaml 中的一等子程序

First-class subroutines in OCaml

OCaml 和其他 ML 方言中的代码类似,但实现使用推理(第 7.2.4 节)在编译时确定fl的类型:

The code in OCaml and other ML dialects is similar, but the implementation uses inference (Section 7.2.4) to determine the types of f and l at compile time:

让 rec apply_to_L fl =

let rec apply_to_L f l =

 匹配 l

 match l with

 | []      -> []

 | []     -> []

 | h :: t -> fh :: apply_to_L ft;; 图片

 | h :: t -> f h :: apply_to_L f t;;

例 9.23

Example 9.23

C 和 C++ 中的子程序指针

Subroutine pointers in C and C++

如第 3.6 节所述,C 和 C++ 不需要子程序闭包,因为它们的子程序不嵌套。指向子程序的简单指针就足够了。这些指针既可以作为参数,也可以作为变量。

As noted in Section 3.6, C and C++ have no need of subroutine closures, because their subroutines do not nest. Simple pointers to subroutines suffice. These are permitted both as parameters and as variables.

void apply_to_A(int (*f)(int),int A[],int A_size) {

void apply_to_A(int (*f)(int), int A[], int A_size) {

 int 我;

 int i;

 对于 (i = 0; i < A_size; i++) A[i] = f(A[i]);

 for (i = 0; i < A_size; i++) A[i] = f(A[i]);

}

}

语法f(n)不仅在f为函数名时使用,而且在f为指向子程序的指针时也使用;指针不需要明确取消引用。■

The syntax f(n) is used not only when f is the name of a function but also when f is a pointer to a subroutine; the pointer need not be dereferenced explicitly. ■

在面向对象语言中,通过将方法及其“环境”打包在显式对象中,可以近似子程序闭包的行为,即使没有嵌套子程序。我们在第 3.6.3 节中描述了这些对象闭包,特别指出了它们与 lambda 表达式和 C++11 中的标准函数类的集成。因为它们是普通对象,所以对象闭包不需要特殊机制将它们作为参数传递或将它们存储在变量中。

In object-oriented languages, one can approximate the behavior of a subroutine closure, even without nested subroutines, by packaging a method and its “environment” within an explicit object. We described these object closures in Section 3.6.3, noting in particular their integration with lambda expressions and the standard function class in C++11. Because they are ordinary objects, object closures require no special mechanisms to pass them as parameters or to store them in variables.

C# 的委托扩展了对象闭包的概念,从而提供类型安全而不受继承的限制。委托不仅可以通过指定的对象方法(包含 C++ 和 Java 的对象闭包)实例化,还可以通过静态函数(包含 C 和 C++ 的子例程指针)或匿名嵌套委托或 lambda 表达式(包含真正的子例程闭包)实例化。如果匿名委托或 lambda 表达式引用周围方法中声明的对象,则这些对象具有无限范围。最后,正如我们将在第 9.6.2 节中看到的那样,C# 委托实际上可以包含一个闭包列表,在这种情况下,调用委托的效果是依次调用列表中的所有条目。(这种行为通常只有当每个条目都有void返回类型时才有意义。它主要用于处理事件。)

The delegates of C# extend the notion of object closures to provide type safety without the restrictions of inheritance. A delegate can be instantiated not only with a specified object method (subsuming the object closures of C++ and Java) but also with a static function (subsuming the subroutine pointers of C and C++) or with an anonymous nested delegate or lambda expression (subsuming true subroutine closures). If an anonymous delegate or lambda expression refers to objects declared in the surrounding method, then those objects have unlimited extent. Finally, as we shall see in Section 9.6.2, a C# delegate can actually contain a list of closures, in which case calling the delegate has the effect of calling all the entries on the list, in turn. (This behavior generally makes sense only when each entry has a void return type. It is used primarily when processing events.)

9.3.2 按姓名呼叫

9.3.2 Call by Name

显式子程序参数并不是唯一需要将闭包作为参数传递的语言特性。通常,当参数的最终使用需要恢复先前的引用环境时,语言实现必须传递闭包。Algol 60 和 Simula 的按名称调用参数、Algol 60 和 Algol 68 的标签参数以及Miranda、Haskell 和 R 的按需要调用参数中都有有趣的例子。

Explicit subroutine parameters are not the only language feature that requires a closure to be passed as a parameter. In general, a language implementation must pass a closure whenever the eventual use of the parameter requires the restoration of a previous referencing environment. Interesting examples occur in the call-by-name parameters of Algol 60 and Simula, the label parameters of Algol 60 and Algol 68, and the call-by-need parameters of Miranda, Haskell, and R.

在09-02-9780124104099 更深入地

IN MORE DEPTH

我们在配套网站上更详细地讨论了按名称调用。当 Algol 60 被定义时,大多数程序员都使用汇编语言进行编程(Fortran 只有几年的历史,而 Lisp 则更新颖)。当时的汇编语言大量使用宏,Algol 设计人员自然而然地提出了一种模仿宏行为的参数传递机制,即正常顺序参数求值(第 6.6.2 节)。考虑到汇编语言中的常见做法,允许goto跳转到作为参数传递的标签也是很自然的。

We consider call by name in more detail on the companion site. When Algol 60 was defined, most programmers programmed in assembly language (Fortran was only a few years old, and Lisp was even newer). The assembly languages of the day made heavy use of macros, and it was natural for the Algol designers to propose a parameter-passing mechanism that mimicked the behavior of macros, namely normal-order argument evaluation (Section 6.6.2). It was also natural, given common practice in assembly language, to allow a goto to jump to a label that was passed as a parameter.

按名称调用参数有一些有趣且强大的应用,但它们比人们最初想象的更难实现(并且使用起来更昂贵):它们需要传递闭包,有时称为thunk。标签参数通常也由闭包实现。按名称调用和标签参数都倾向于导致难以理解的代码;现代语言通常鼓励程序员改用显式的形式子程序和结构化异常。值得注意的是,大多数反对按名称调用的论点在纯函数式代码中都消失了,其中副作用自由确保参数的值无论何时评估都始终相同。利用这一观察,Haskell(及其前身 Miranda)对所有参数采用正常顺序评估。

Call-by-name parameters have some interesting and powerful applications, but they are more difficult to implement (and more expensive to use) than one might at first expect: they require the passing of closures, sometimes referred to as thunks. Label parameters are typically implemented by closures as well. Both call-by-name and label parameters tend to lead to inscrutable code; modern languages typically encourage programmers to use explicit formal subroutines and structured exceptions instead. Significantly, most of the arguments against call by name disappear in purely functional code, where side-effect freedom ensures that the value of a parameter will always be the same regardless of when it is evaluated. Leveraging this observation, Haskell (and its predecessor Miranda) employs normal-order evaluation for all parameters.

9.3.3 特殊用途参数

9.3.3 Special-Purpose Parameters

图 9.3总结了常见的参数传递模式。本节我们将研究参数传递的其他方面。

Figure 9.3 contains a summary of the common parameter-passing modes. In this subsection we examine other aspects of parameter passing.

编号09-03-9780124104099
图 9.3 参数传递模式。第 1 列表示模式的通用名称。第 2 列表示使用这些模式或引入这些模式的著名语言。第 3 列表示通过传递值、引用或闭包来实现。第 4 列表示被调用者是否可以读取或写入形式参数第 5 列表示对形式参数的更改是否会影响实际参数第 6 列表示在子例程执行期间,对形式参数或实际参数的更改是否可以通过另一个看到。*如果程序在调用后尝试使用右值参数,则行为未定义。在 R 中,对按需传递的参数的更改只会在第一次使用时发生;在 Haskell 中不允许进行更改。

默认(可选)参数

Default (Optional) Parameters

3.3.6 节中,我们注意到默认参数为改变子程序的行为提供了一种有吸引力的动态作用域替代方案。默认参数是调用者不必提供的参数;如果缺少该参数,则将使用预先设定的默认值。

In Section 3.3.6, we noted that default parameters provide an attractive alternative to dynamic scope for changing the behavior of a subroutine. A default parameter is one that need not necessarily be provided by the caller; if it is missing, then a preestablished default value will be used instead.

例 9.24

Example 9.24

Ada 中的默认参数

Default parameters in Ada

默认参数的一个常见用途是在 I/O 库例程中(如第 C-8.7.3 节所述)。例如,在 Ada 中,整数的put例程在text_IO库包中有以下声明:

One common use of default parameters is in I/O library routines (described in Section C-8.7.3). In Ada, for example, the put routine for integers has the following declaration in the text_IO library package:

类型字段是整数范围0..integer'last;

type field is integer range 0..integer'last;

类型 number_base 是整数范围 2..16;

type number_base is integer range 2..16;

默认宽度:字段     :=整数'宽度;

default_width : field     := integer'width;

默认基数:数字基数:= 10;

default_base : number_base := 10;

程序 put(项目:整数;

procedure put(item : in integer;

     宽度:在字段中     :=默认宽度;

     width : in field     := default_width;

     基数:在 number_base := default_base 中);

     base : in number_base := default_base);

这里default_width的声明使用内置类型属性 width来确定在当前机器上以十进制打印整数所需的最大列数(例如,32 位整数需要不超过 11 列,包括可选的减号)。

Here the declaration of default_width uses the built-in type attribute width to determine the maximum number of columns required to print an integer in decimal on the current machine (e.g., a 32-bit integer requires no more than 11 columns, including the optional minus sign).

在 Ada 中,任何在子程序标题中“分配”值的形式参数都是可选的。在我们的text_IO示例中,程序员可以使用一个、两个或三个参数调用put 。无论在特定调用中提供了多少个参数, put的代码始终可以假定它具有所有三个参数。实现很简单:在任何缺少实际参数的调用中,编译器都假装已经提供了默认值;它生成一个调用序列,将这些默认值加载到寄存器中或将它们推送到堆栈上,根据需要。在 32 位机器上,put(37)将以十进制表示法在 11 列字段(带有 9 个前导空格)中打印字符串“37”。Put (37, 4)将以四列字段(两个前导空格)中打印“37”,而put(37, 4, 8)将以四列字段中打印“45”(37 = 45 8 )。

Any formal parameter that is “assigned” a value in its subroutine heading is optional in Ada. In our text_IO example, the programmer can call put with one, two, or three arguments. No matter how many are provided in a particular call, the code for put can always assume it has all three parameters. The implementation is straightforward: in any call in which actual parameters are missing, the compiler pretends as if the defaults had been provided; it generates a calling sequence that loads those defaults into registers or pushes them onto the stack, as appropriate. On a 32-bit machine, put(37) will print the string “37” in an 11-column field (with nine leading blanks) in base-10 notation. Put(37, 4) will print “37” in a four-column field (two leading blanks), and put(37, 4, 8) will print “45” (37 = 458) in a four-column field.

由于default_widthdefault_base变量是text_IO接口的一部分,程序员可以根据需要更改它们。在缺少实际值的调用中使用默认值时,编译器会从包的变量中加载默认值。如第 C-8.7.3 节所述,所有内置类型都有put的重载实例。实际上,每种类型都有两个put的重载实例,其中一个具有附加的第一个参数,用于指定要将值写入的输出文件。4应该强调的是,就默认参数而言,I/O 没有什么特别之处:默认值可用于任何子例程声明中。除了 Ada,默认参数还出现在 C++、C#、Common Lisp、Fortran 90 和 Python 中。■

Because the default_width and default_base variables are part of the text_IO interface, the programmer can change them if desired. When using default values in calls with missing actuals, the compiler loads the defaults from the variables of the package. As noted in Section C-8.7.3, there are overloaded instances of put for all the built-in types. In fact, there are two overloaded instances of put for every type, one of which has an additional first parameter that specifies the output file to which to write a value.4 It should be emphasized that there is nothing special about I/O as far as default parameters are concerned: defaults can be used in any subroutine declaration. In addition to Ada, default parameters appear in C++, C#, Common Lisp, Fortran 90, and Python. ■

命名参数

Named Parameters

例 9.25

Example 9.25

Ada 中的命名参数

Named parameters in Ada

到目前为止,在我们所有的讨论中,我们都假设参数是位置相关的:第一个实际参数对应于第一个形式参数,第二个实际参数对应于第二个形式参数,依此类推。在某些语言中,包括 Ada、C#、Common Lisp、Fortran 90、Python 和 Swift,情况不必如此。这些语言允许对参数进行命名。命名参数(也称为关键字参数)与默认参数结合使用时特别有用。位置表示法允许我们编写put(37, 4)来在四列字段中打印“37”,但它不允许我们在默认宽度的字段中以八进制打印:任何指定基数的调用(使用位置表示法)也必须明确指定宽度,因为在put的参数列表中,宽度参数位于基数之前。命名参数为 Ada 程序员提供了一种解决这个问题的方法:

In all of our discussions so far we have been assuming that parameters are positional: the first actual parameter corresponds to the first formal parameter, the second actual to the second formal, and so on. In some languages, including Ada, C#, Common Lisp, Fortran 90, Python, and Swift, this need not be the case. These languages allow parameters to be named. Named parameters (also called keyword parameters) are particularly useful in conjunction with default parameters. Positional notation allows us to write put(37, 4) to print “37” in a four-column field, but it does not allow us to print in octal in a field of default width: any call (with positional notation) that specifies a base must also specify a width, explicitly, because the width parameter precedes the base in put's parameter list. Named parameters provide the Ada programmer with a way around this problem:

放入(物品 => 37,基础 => 8);

put(item => 37, base => 8);

因为参数是有命名的,所以它们的顺序并不重要;我们也可以写

Because the parameters are named, their order does not matter; we can also write

放入(基数 => 8,项目 => 37);

put(base => 8, item => 37);

我们甚至可以混合这两种方法,对前几个参数使用位置表示法,对其余所有参数使用名称:

We can even mix the two approaches, using positional notation for the first few parameters, and names for all the rest:

放入(37,base => 8); 图片

put(37, base => 8);

例 9.26

Example 9.26

使用命名参数进行自我文档化

Self-documentation with named parameters

除了允许按任意顺序指定参数、省略任何不需要特殊值的中间默认参数之外,命名参数表示法的优点是可以记录每个参数的用途。对于具有大量参数的子程序,可能很难记住哪个是哪个。命名表示法可以在调用中明确参数的含义,如以下假设示例所示:

In addition to allowing parameters to be specified in arbitrary order, omitting any intermediate default parameters for which special values are not required, named parameter notation has the advantage of documenting the purpose of each parameter. For a subroutine with a very large number of parameters, it can be difficult to remember which is which. Named notation makes the meaning of arguments explicit in the call, as in the following hypothetical example:

format_page(列 => 2,

format_page(columns => 2,

 窗口高度 => 400,窗口宽度 => 200,

 window_height => 400, window_width => 200,

 header_font => Helvetica, body_font => Times,

 header_font => Helvetica, body_font => Times,

 title_font => Times_Bold, header_point_size => 10,

 title_font => Times_Bold, header_point_size => 10,

 主体点大小 => 11, 标题点大小 => 13,

 body_point_size => 11, title_point_size => 13,

 理由 => true, 连字 => false,

 justification => true, hyphenation => false,

 页码 => 3, 段落缩进 => 18,

 page_num => 3, paragraph_indent => 18,

 背景颜色 => 白色); 图片

 background_color => white);

可变数量的参数

Variable Numbers of Arguments

包括 Lisp、C 及其后代以及大多数脚本语言在内的多种语言都允许用户定义采用可变数量参数的子例程。此类子例程的示例可在 C-8.7.3 节中找到: C 的stdio I/O 库的printfscanf函数。在 C 中,printf可以声明如下:

Several languages, including Lisp, C and its descendants, and most of the scripting languages, allow the user to define subroutines that take a variable number of arguments. Examples of such subroutines can be found in Section C-8.7.3: the printf and scanf functions of C's stdio I/O library. In C, printf can be declared as follows:

int printf(char *格式,…){

int printf(char *format, …) {

函数头中的省略号 (…) 是语言语法的一部分。它表示格式后面有附加参数,但未指定其类型和数量。由于 C 和 C++ 是静态类型的,因此附加参数不是类型安全的。然而,由于动态类型,它们在 Common Lisp 和脚本语言中是类型安全的。

The ellipsis (…) in the function header is a part of the language syntax. It indicates that there are additional parameters following the format, but that their types and numbers are unspecified. Since C and C++ are statically typed, additional parameters are not type safe. They are type safe in Common Lisp and the scripting languages, however, thanks to dynamic typing.

例 9.27

Example 9.27

C 语言中可变数量的参数

Variable number of arguments in C

在具有可变长度参数列表的函数主体中,C 或 C++ 程序员必须使用一组标准例程来访问额外的参数。这些例程最初定义为宏,其实现因机器而异,具体取决于参数传递给函数的方式;如今,必要的支持通常内置在编译器中。对于printf,可变参数在 C 中的使用方式如下:

Within the body of a function with a variable-length argument list, the C or C++ programmer must use a collection of standard routines to access the extra arguments. Originally defined as macros, these routines have implementations that vary from machine to machine, depending on how arguments are passed to functions; today the necessary support is usually built into the compiler. For printf, variable arguments would be used as follows in C:

#include <stdarg.h>      /* 宏和类型定义 */

#include <stdarg.h>     /* macros and type definitions */

int printf(char *格式,…){

int printf(char *format, …) {

 va_list 参数;

 va_list args;

 va_start(参数,格式);

 va_start(args, format);

 …

 …

  char cp = va_arg(args,char);

  char cp = va_arg(args, char);

  …

  …

  双精度 dp = va_arg(args,双精度);

  double dp = va_arg(args, double);

 …

 …

 va_end(参数);

 va_end(args);

}

}

此处,args被定义为va_list类型的对象,这是一种用于枚举省略的参数的特殊(依赖于实现)类型。va_start例程将最后一个声明的参数(在本例中为format)作为其第二个参数。它初始化其第一个参数(在本例中为args),以便可以使用它来枚举调用方的其余实际参数。必须声明至少一个形式参数;它们不能全部被省略。

Here args is defined as an object of type va_list, a special (implementation-dependent) type used to enumerate the elided parameters. The va_start routine takes the last declared parameter (in this case, format) as its second argument. It initializes its first argument (in this case args) so that it can be used to enumerate the rest of the caller's actual parameters. At least one formal parameter must be declared; they can't all be elided.

每次调用va_arg都会返回下一个省略的参数的值。上面给出了两个例子。每个例子都指定了参数的预期类型,并将结果赋给适当类型的变量。如果预期类型与实际参数的类型不同,则会导致混乱。在printf中,格式字符串中的%X占位符用于确定类型:printf包含一个大型switch语句,每个可能的X都有一个分支。 %c的分支包含对va_arg(args, char)的调用; %f的分支包含对va_arg(args, double)的调用。所有 C 浮点类型在传递给子程序之前都会扩展为双精度,因此printf内部无需担心floatdouble之间的区别。另一方面,Scanf必须区分指向浮点数的指针和指向double 的指针。对va_end的调用允许实现执行任何必要的清理操作(例如,释放用于va_list的任何堆空间,或修复任何可能混淆结尾代码的堆栈框架的更改)。■

Each call to va_arg returns the value of the next elided parameter. Two examples appear above. Each specifies the expected type of the parameter, and assigns the result into a variable of the appropriate type. If the expected type is different from the type of the actual parameter, chaos can result. In printf, the %X placeholders in the format string are used to determine the type: printf contains a large switch statement, with one arm for each possible X. The arm for %c contains a call to va_arg(args, char); the arm for %f contains a call to va_arg(args, double). All C floating-point types are extended to double-precision before being passed to a subroutine, so there is no need inside printf to worry about the distinction between floats and doubles. Scanf, on the other hand, must distinguish between pointers to floats and pointers to doubles. The call to va_end allows the implementation to perform any necessary cleanup operations (e.g., deallocation of any heap space used for the va_list, or repair of any changes to the stack frame that might confuse the epilogue code). ■

例 9.28

Example 9.28

Java 中可变数量的参数

Variable number of arguments in Java

与 C 和 C++ 一样,C# 和 Java 的最新版本也支持可变数量的参数,但与它们的父语言不同,它们以类型安全的方式实现这一点,即要求所有尾随参数共享一个通用类型。例如,在 Java 中,可以这样写

Like C and C++, C# and recent versions of Java support variable numbers of parameters, but unlike their parent languages they do so in a type-safe manner, by requiring all trailing parameters to share a common type. In Java, for example, one can write

静态 void print_lines(String foo,String…lines){

static void print_lines(String foo, String… lines) {

 System.out.println(“第一个参数是\”” + foo + “\”。”);

 System.out.println(“First argument is \”” + foo + “\”.”);

 System.out.println(“有”+

 System.out.println(“There are ” +

  lines.length + “ 附加参数:”);

  lines.length + “ additional arguments:”);

 对于(字符串 str:行){

 for (String str: lines) {

  系统.out.println(str);

  System.out.println(str);

 }

 }

}

}

print_lines(“你好,世界”,“这是一条消息”,“来自您的赞助商。”)

print_lines(“Hello, world”, “This is a message”, “from your sponsor.”);

这里方法头中的省略号再次成为语言语法的一部分。方法print_lines有两个参数。第一个参数fooString类型;第二个参数 linesString…类型。在print_lines中,lines函数就像是String[]类型( String数组)一样。但是,调用者不需要将第二个和后续参数打包成显式数组;编译器会自动执行此操作,程序会打印

Here again the ellipsis in the method header is part of the language syntax. Method print_lines has two arguments. The first, foo, is of type String; the second, lines, is of type String…. Within print_lines, lines functions as if it had type String[] (array of String). The caller, however, need not package the second and subsequent parameters into an explicit array; the compiler does this automatically, and the program prints

第一个参数是“Hello, world”。

First argument is “Hello, world”.

还有 2 个附加参数:

There are 2 additional arguments:

这是一条消息

This is a message

来自你的赞助商。 图片

from your sponsor.

例 9.29

Example 9.29

C# 中可变数量的参数

Variable number of arguments in C#

在 C# 中,参数声明语法略有不同:

The parameter declaration syntax is slightly different in C#:

静态 void print_lines(String foo,params String[] lines) {

static void print_lines(String foo, params String[] lines) {

 Console.WriteLine(“第一个参数是 \”” + foo + “\”。”);

 Console.WriteLine(“First argument is \”” + foo + “\”.”);

 Console.WriteLine(“有 ” +

 Console.WriteLine(“There are ” +

  lines.Length + “附加参数:”);

  lines.Length + “ additional arguments:”);

 foreach(字符串行数){

 foreach (String line in lines) {

  控制台.WriteLine(行);

  Console.WriteLine(line);

 }

 }

}

}

调用语法相同■

The calling syntax is the same ■

9.3.4 函数返回

9.3.4 Function Returns

函数指示要返回的值的语法差异很大。在 Lisp、ML 和 Algol 68 等不区分表达式和语句的语言中,函数的值只是其主体的值,而主体本身就是一个表达式。

The syntax by which a function indicates the value to be returned varies greatly. In languages like Lisp, ML, and Algol 68, which do not distinguish between expressions and statements, the value of a function is simply the value of its body, which is itself an expression.

例 9.30

Example 9.30

return语句

return statement

在几种早期的命令式语言中,包括 Algol 60、Fortran 和 Pascal,函数通过执行赋值语句来指定其返回值,赋值语句的左侧是函数的名称。这种方法与通常的静态作用域规则(第 3.3.1 节)存在不良影响:编译器必须禁止任何会隐藏函数名称的直接嵌套声明,因为这样函数就无法返回。在较新的命令式语言中,通过引入显式返回语句可以避免这种特殊情况:

In several early imperative languages, including Algol 60, Fortran, and Pascal, a function specified its return value by executing an assignment statement whose left-hand side was the name of the function. This approach has an unfortunate interaction with the usual static scope rules (Section 3.3.1): the compiler must forbid any immediately nested declaration that would hide the name of the function, since the function would then be unable to return. This special case is avoided in more recent imperative languages by introducing an explicit return statement:

返回表达式

return expression

除了指定值之外,return还会立即终止子程序。如果函数已经确定要返回什么,但还不想返回,那么它总是可以将返回值赋给临时变量,然后再返回:

In addition to specifying a value, return causes the immediate termination of the subroutine. A function that has figured out what to return but doesn't want to return yet can always assign the return value into a temporary variable, and then return it later:

rtn :=表达式

rtn := expression

返回 rtn 图片

return rtn

Fortran 将子程序的终止与返回值的指定分开:它通过分配给函数名称来指定返回值,并且具有不带任何参数的返回语句。

Fortran separates termination of a subroutine from the specification of return values: it specifies the return value by assigning to the function name, and has a return statement that takes no arguments.

例 9.31

Example 9.31

返回值的增量计算

Incremental computation of a return value

带参数的返回语句和对函数名的赋值都迫使程序员在增量计算中使用临时变量。以下是 Ada 中的一个例子:

Argument-bearing return statements and assignment to the function name both force the programmer to employ a temporary variable in incremental computations. Here is an example in Ada:

类型 int_array 是整数数组(整数范围 <>);

type int_array is array (integer range <>) of integer;

 -- 未指定整数边界的整数数组

 -- array of integers with unspecified integer bounds

函数 A_max(A : int_array) 返回整数是

function A_max(A : int_array) return integer is

rtn:整数;

rtn : integer;

开始

begin

 rtn := 整数'第一个;

 rtn := integer'first;

 for i in A'first..A'last 循环

 for i in A'first .. A'last loop

  如果 A(i) > rtn 则 rtn := A(i); 结束如果;

  if A(i) > rtn then rtn := A(i); end if;

 结束循环;

 end loop;

 返回 rtn;

 return rtn;

结束A_max;

end A_max;

这里必须将rtn声明为变量,以便函数可以读取和写入它。由于rtn是局部变量,因此大多数编译器会将其分配在A_max的堆栈框架内。然后, return语句必须将该变量的值复制到调用者分配的返回位置。■

Here rtn must be declared as a variable so that the function can read it as well as write it. Because rtn is a local variable, most compilers will allocate it within the stack frame of A_max. The return statement must then copy that variable's value into the return location allocated by the caller. ■

例 9.32

Example 9.32

Go 中明确命名的返回值

Explicitly named return values in Go

有些语言允许函数的结果有自己的名字,从而消除了对局部变量的需求。在 Go 中,你可以这样写

Some languages eliminate the need for a local variable by allowing the result of a function to have a name in its own right. In Go one can write

func A_max(A []int) (rtn int) {

func A_max(A []int) (rtn int) {

 rtn = A[0]

 rtn = A[0]

 对于 i := 1; i < len(A); i++ {

 for i := 1; i < len(A); i++ {

  如果 A[i] > rtn { rtn = A[i] }

  if A[i] > rtn { rtn = A[i] }

 }

 }

 返回

 return

}

}

这里rtn可以在整个生命周期中驻留在调用者分配的返回位置中。在 Eiffel 中可以找到类似的功能,其中每个函数都包含一个名为Result的隐式声明对象。此对象可读可写,并在函数返回时返回给调用者。■

Here rtn can reside throughout its lifetime in the return location allocated by the caller. A similar facility can be found in Eiffel, in which every function contains an implicitly declared object named Result. This object can be both read and written, and is returned to the caller when the function returns. ■

例 9.33

Example 9.33

多值返回

Multivalue returns

许多早期语言对函数可以返回的对象类型进行了限制。在 Algol 60 和 Fortran 77 中,函数必须返回标量值。在 Pascal 和早期版本的 Modula-2 中,它可以返回标量或指针。大多数命令式语言都更灵活:Algol 68、Ada、C、Fortran 90 和许多(非标准)Pascal 实现允许函数返回复合类型的值。ML、其后代和几种脚本语言允许函数返回一个值元。例如,在 Python 中,我们可以写

Many early languages placed restrictions on the types of objects that could be returned from a function. In Algol 60 and Fortran 77, a function had to return a scalar value. In Pascal and early versions of Modula-2, it could return a scalar or a pointer. Most imperative languages are more flexible: Algol 68, Ada, C, Fortran 90, and many (nonstandard) implementations of Pascal allow functions to return values of composite type. ML, its descendants, and several scripting languages allow a function to return a tuple of values. In Python, for example, we might write

定义 foo():

def foo():

 返回 2, 3

 return 2, 3

我,j = foo() 图片

i, j = foo()

在函数式语言中,将子程序作为闭包返回是很常见的。许多命令式语言也允许这样做。C 没有闭包,但允许函数返回指向子程序的指针。

In functional languages, it is commonplace to return a subroutine as a closure. Many imperative languages permit this as well. C has no closures, but allows a function to return a pointer to a subroutine.

在09-01-9780124104099检查你的理解

Check Your Understanding

13. 形式参数实际参数有什么区别?

13. What is the difference between formal and actual parameters?

14. 描述四种常见的参数传递模式。程序员如何选择何时使用哪一种?

14. Describe four common parameter-passing modes. How does a programmer choose which one to use when?

15.解释 Modula-3 中READONLY参数的原理。

15. Explain the rationale for READONLY parameters in Modula-3.

16. 在具有变量参考模型的语言中,通常使用哪种参数模式?

16. What parameter mode is typically used in languages with a reference model of variables?

17. 描述一下 Ada 的参数模式。它们与其他现代语言的模式有何不同?

17. Describe the parameter modes of Ada. How do they differ from the modes of other modern languages?

18. 给出一个例子,说明在 C++ 中从函数返回引用是有用的。

18. Give an example in which it is useful to return a reference from a function in C++.

19. 什么是r 值引用?它为什么有用?

19. What is an r-value reference?. Why might it be useful?

20. 列出语言实现可能将参数实现为闭包的三个原因。

20. List three reasons why a language implementation might implement a parameter as a closure.

21. 什么是一致开放阵列

21. What is a conformant (open) array?

22. 什么是默认参数?它们是如何实现的?

22. What are default parameters? How are they implemented?

23. 什么是命名关键字参数? 它们有什么用处?

23. What are named (keyword) parameters? Why are they useful?

24. 解释可变长度参数列表的价值。Java 和 C# 中的此类列表与 C 和 C++ 中的列表有何区别?

24. Explain the value of variable-length argument lists. What distinguishes such lists in Java and C# from their counterparts in C and C++?

25. 描述三种指定函数返回值的常见机制。它们的相对优点和缺点是什么?

25. Describe three common mechanisms for specifying the return value of a function. What are their relative strengths and drawbacks?

9.4 异常处理

9.4 Exception Handling

我们在前面的章节中多次提到了异常处理机制。我们之所以推迟到现在才详细讨论这些机制,是因为异常处理通常需要语言实现来“展开”子程序调用堆栈。

Several times in the preceding chapters and sections we have referred to exception-handling mechanisms. We have delayed detailed discussion of these mechanisms until now because exception handling generally requires the language implementation to “unwind” the subroutine call stack.

异常可以定义为程序执行过程中出现的意外(或至少是不寻常)情况,并且无法在本地上下文中轻松处理。它可能由语言实现自动检测到,或者程序可能显式地引发抛出它(这两个术语是同义词)。最常见的异常是各种运行时错误。例如,在 I/O 库中,输入例程可能在读取请求的值之前遇到文件末尾,或者它可能在文件末尾找到标点符号或字母当输入数字时,程序会返回错误。为了在没有异常处理机制的情况下处理此类错误,程序员基本上有三个选择,但没有一个是完全令人满意的:

An exception can be defined as an unexpected—or at least unusual—condition that arises during program execution, and that cannot easily be handled in the local context. It may be detected automatically by the language implementation, or the program may raise or throw it explicitly (the two terms are synonymous). The most common exceptions are various sorts of run-time errors. In an I/O library, for example, an input routine may encounter the end of its file before it can read a requested value, or it may find punctuation marks or letters on the input when it is expecting digits. To cope with such errors without an exception-handling mechanism, the programmer has basically three options, none of which is entirely satisfactory:

1. 当无法返回真实值时,“发明”一个可供调用者使用的值。

1. “Invent” a value that can be used by the caller when a real value could not be returned.

2. 向调用者返回一个显式的“状态”值,调用者必须在每次调用后检查该值。状态通常通过一个额外的显式参数传递。在某些语言中,常规返回值和状态可能会作为元组一起返回。

2. Return an explicit “status” value to the caller, who must inspect it after every call. Most often, the status is passed through an extra, explicit parameter. In some languages, the regular return value and the status may be returned together as a tuple.

3. 依靠调用者传递一个闭包(在支持它们的语言中)来进行错误处理例程,当正常例程遇到问题时可以调用该例程。

3. Rely on the caller to pass a closure (in languages that support them) for an error-handling routine that the normal routine can call when it runs into trouble.

在某些情况下,第一个选项是可行的,但在一般情况下不起作用。选项 2 和 3 往往会使程序变得混乱,并带来我们通常希望避免的开销。选项 2 中的测试尤其令人反感:它们掩盖了通常情况下的正常事件流。由于它们非常繁琐和重复,因此它们也是常见的错误来源;人们很容易忘记所需的测试。异常处理机制通过将错误检查代码“移出行外”来解决这些问题,允许简单地指定正常情况,并在适当的时候安排控制分支到处理程序

The first of these options is fine in certain cases, but does not work in the general case. Options 2 and 3 tend to clutter up the program, and impose overhead that we should like to avoid in the common case. The tests in option 2 are particularly offensive: they obscure the normal flow of events in the common case. Because they are so tedious and repetitive, they are also a common source of errors; one can easily forget a needed test. Exception-handling mechanisms address these issues by moving error-checking code “out of line,” allowing the normal case to be specified simply, and arranging for control to branch to a handler when appropriate.

例 9.34

Example 9.34

PL/I 中的ON条件

ON conditions in PL/I

异常处理是由 PL/I 率先提出的,它包含以下形式的可执行语句

Exception handling was pioneered by PL/I, which includes an executable statement of the form

ON条件

ON condition

           陈述

           statement

嵌套语句(通常是GOTOBEGIN…END块)是处理程序。遇到ON语句时不会执行它,但会“记住”以供将来参考。如果出现异常情况(例如OVERFLOW),它将在稍后执行。由于ON语句是可执行的,因此处理程序与异常的绑定取决于运行时的控制流。■

The nested statement (often a GOTO or a BEGIN…END block) is a handler. It is not executed when the ON statement is encountered, but is “remembered” for future reference. It will be executed later if exception condition (e.g., OVERFLOW) arises. Because the ON statement is executable, the binding of handlers to exceptions depends on the flow of control at run time. ■

如果调用 PL/I 异常处理程序然后“返回”(即不执行 GOTO程序中的其他位置),则会发生以下两种情况之一。对于语言设计者认为是致命的异常,程序本身将终止。对于“可恢复”异常,执行将在发生异常的语句之后的语句处恢复。不幸的是,PL/I 的经验表明,处理程序与异常的动态绑定以及发生异常的代码的自动恢复都令人困惑且容易出错。

If a PL/I exception handler is invoked and then “returns” (i.e., does not perform a GOTO to somewhere else in the program), then one of two things will happen. For exceptions that the language designers considered to be fatal, the program itself will terminate. For “recoverable” exceptions, execution will resume at the statement following the one in which the exception occurred. Unfortunately, experience with PL/I revealed that both the dynamic binding of handlers to exceptions and the automatic resumption of code in which an exception occurred were confusing and error-prone.

例 9.35

Example 9.35

C++ 中的简单try块

A simple try block in C++

许多较新的语言,包括 Ada、Python、PHP、Ruby、C++、Java、C# 和 ML,都提供了异常处理功能,其中处理程序在词汇上与代码块绑定,并且处理程序的执行会替换代码块中尚未完成的部分。在 C++ 中,我们可以这样写

Many more recent languages, including Ada, Python, PHP, Ruby, C++, Java, C#, and ML, provide exception-handling facilities in which handlers are lexically bound to blocks of code, and in which the execution of the handler replaces the yet-to-be-completed portion of the block. In C++ we might write

尝试 {

try {

 

 

 如果(出现意外情况)

 if (something_unexpected)

  抛出我的错误(“哎呀!”);

  throw my_error(“oops!”);

 …

 …

 cout << “一切正常\n”;

 cout << “everything's ok\n”;

 …

 …

} 捕获 (我的错误 e) {

} catch (my_error e) {

 cout<<e.解释<<“\n”;

 cout << e.explanation << “\n”;

}

}

如果发生something_unexpected,此代码将抛出my_error类的异常。此异常将被catch捕获,其参数e具有匹配的类型(此处假设具有一个名为interpretation的字符串字段)。然后,catch 块将代替try块的其余部分执行。■

If something_unexpected occurs, this code will throw an exception of class my_error. This exception will be caught by the catch block, whose parameter, e, has a matching type (here assumed to have a string field named explanation). The catch block will then execute in place of the remainder of the try block. ■

例 9.36

Example 9.36

嵌套try

Nested try blocks

带有处理程序的代码块可以嵌套:

Code blocks with handlers can nest:

尝试 {

try {

 

 

 尝试 {

 try {

  …

  …

  如果(出现意外情况)

  if (something_unexpected)

   抛出我的错误(“哎呀!”);

   throw my_error(“oops!”);

  …

  …

  cout << “一切正常\n”;

  cout << “everything's ok\n”;

  …

  …

 } 捕获(some_other_error e1){

 } catch (some_other_error e1) {

  cout << “不是这个\n”;

  cout << “not this one\n”;

 }

 }

 …

 …

} 捕获 (我的错误 e) {

} catch (my_error e) {

 cout<<e.解释<<“\n”;

 cout << e.explanation << “\n”;

}

}

当引发异常时,控制权将转移到当前子程序中最内层的匹配处理程序。■

When the exception is thrown, control transfers to the innermost matching handler within the current subroutine. ■

例 9.37

Example 9.37

将异常从被调用例程中传播出去

Propagation of an exception out of a called routine

如果当前子程序中没有匹配的处理程序,则子程序会突然返回,并在调用点重新引发异常:

If there is no matching handler in the current subroutine, then the subroutine returns abruptly and the exception is re raised at the point of call:

尝试 {

try {

 …

 …

 foo();

 foo();

 …

 …

 cout << “一切正常\n”;

 cout << “everything's ok\n”;

 …

 …

} 捕获 (我的错误 e) {

} catch (my_error e) {

 cout<<e.解释<<“\n”;

 cout << e.explanation << “\n”;

}

}

无效 foo() {

void foo() {

 …

 …

 如果(出现意外情况)

 if (something_unexpected)

  抛出我的错误(“哎呀!”);

  throw my_error(“oops!”);

 …

 …

}

}

如果调用例程未处理异常,则继续沿动态链向上传播。如果程序的主例程未处理异常,则调用预定义的最外层处理程序,通常会终止程序。■

If the exception is not handled in the calling routine, it continues to propagate back up the dynamic chain. If it is not handled in the program's main routine, then a predefined outermost handler is invoked, and usually terminates the program. ■

从某种意义上说,异常处理对子程序调用顺序的依赖性可以被视为一种动态绑定形式,但它比 PL/I 中的形式要严格得多。与其说调用例程中的处理程序已动态绑定到被调用例程中的错误,我们更愿意说处理程序在词汇上绑定到调用被调用例程的表达式或语句。然后,被调用例程中未处理的异常可以建模为“异常返回”;它会导致调用表达式或语句引发异常,而该异常又在其子例程中在词汇上进行处理。

In a sense, the dependence of exception handling on the order of subroutine calls might be considered a form of dynamic binding, but it is a much more restricted form than is found in PL/I. Rather than say that a handler in a calling routine has been dynamically bound to an error in a called routine, we prefer to say that the handler is lexically bound to the expression or statement that calls the called routine. An exception that is not handled inside a called routine can then be modeled as an “exceptional return”; it causes the calling expression or statement to raise an exception, which is again handled lexically within its subroutine.

实际上,异常处理程序倾向于执行三种操作。首先,理想情况下,处理程序将以允许程序恢复并继续执行的方式补偿异常。例如,为了响应存储管理例程中的“内存不足”异常,处理程序可能会要求操作系统为应用程序分配更多空间,之后它可以完成请求的操作。其次,当给定代码块中发生异常但无法在本地处理时,通常重要的是声明一个本地处理程序来清理本地块中分配的任何资源,然后“重新引发”异常,以便它将继续传播回可以(希望)恢复的处理程序。第三,如果无法恢复,处理程序至少可以在程序终止之前打印一条有用的错误消息。

In practice, exception handlers tend to perform three kinds of operations. First, ideally, a handler will compensate for the exception in a way that allows the program to recover and continue execution. For example, in response to an “out of memory” exception in a storage management routine, a handler might ask the operating system to allocate additional space to the application, after which it could complete the requested operation. Second, when an exception occurs in a given block of code but cannot be handled locally, it is often important to declare a local handler that cleans up any resources allocated in the local block, and then “reraises” the exception, so that it will continue to propagate back to a handler that can (hopefully) recover. Third, if recovery is not possible, a handler can at least print a helpful error message before the program terminates.

如第 6.2.1 节所述,异常与多级返回的概念相关,但又有区别。执行多级返回的例程按预期运行;用 Eiffel 术语来说,它正在履行其契约。引发异常的例程未按预期运行;它无法履行其契约。Common Lisp 和 Ruby 区分这两个相关概念,但大多数语言没有;在大多数情况下,多级返回需要外部调用者提供一个简单的处理程序。

As discussed in Section 6.2.1, exceptions are related to, but distinct from, the notion of multilevel returns. A routine that performs a multilevel return is functioning as expected; in Eiffel terminology, it is fulfilling its contract. A routine that raises an exception is not functioning as expected; it cannot fulfill its contract. Common Lisp and Ruby distinguish between these two related concepts, but most languages do not; in most, a multilevel return requires the outer caller to provide a trivial handler.

Common Lisp 的不同寻常之处还在于它提供了四种不同版本的异常处理机制。其中两个版本提供通常的“异常返回”语义;其他版本旨在修复问题并重新开始某个动态封闭表达式的求值。正交地,两个版本在声明处理程序的引用环境中执行工作;其他版本在异常首次发生的环境中执行工作。后一种选择允许抽象提供几种从异常中恢复的备选策略。然后,抽象的用户可以动态指定在给定上下文中应使用哪种策略。我们将在练习 9.22探索 9.43中进一步考虑 Common Lisp 。在处理程序环境中执行工作的“异常返回”机制称为handler-case;它提供的语义与大多数其他现代语言相当。

Common Lisp is also unusual in providing four different versions of its exception-handling mechanism. Two of these provide the usual “exceptional return” semantics; the others are designed to repair the problem and restart evaluation of some dynamically enclosing expression. Orthogonally, two perform their work in the referencing environment where the handler is declared; the others perform their work in the environment where the exception first arises. The latter option allows an abstraction to provide several alternative strategies for recovery from exceptions. The user of the abstraction can then specify, dynamically, which of these strategies should be used in a given context. We will consider Common Lisp further in Exercise 9.22 and Exploration 9.43. The “exceptional return” mechanism, with work performed in the environment of the handler, is known as handler-case; it provides semantics comparable to those of most other modern languages.

9.4.1 定义异常

9.4.1 Defining Exceptions

在许多语言中,动态语义错误会自动导致异常,然后程序可以捕获这些异常。程序员还可以定义其他特定于应用程序的异常。预定义异常的示例包括算术溢出、除以零、输入时文件末尾、下标和子范围错误以及空指针取消引用。将这些定义为异常(而不是致命错误)的理由是它们可能出现在某些有效程序中。在大多数语言中,一些其他动态错误(例如,从尚未指定返回值的子程序返回)仍然是致命的。在 C++ 和 Common Lisp 中,所有异常都是程序员定义的。在 PHP 中,set_error_handler函数可用于将内置语义错误转换为普通异常。在 Ada 中,一些预定义异常可以通过编译指示来抑制。

In many languages, dynamic semantic errors automatically result in exceptions, which the program can then catch. The programmer can also define additional, application-specific exceptions. Examples of predefined exceptions include arithmetic overflow, division by zero, end-of-file on input, subscript and subrange errors, and null pointer dereference. The rationale for defining these as exceptions (rather than as fatal errors) is that they may arise in certain valid programs. Some other dynamic errors (e.g., return from a subroutine that has not yet designated a return value) are still fatal in most languages. In C++ and Common Lisp, exceptions are all programmer defined. In PHP, the set_error_handler function can be used to turn built-in semantic errors into ordinary exceptions. In Ada, some of the predefined exceptions can be suppressed by means of a pragma.

例 9.38

Example 9.38

什么例外?

What is an exception?

Ada 异常只是内置异常类型的对象:

An Ada exception is simply an object of the built-in exception type:

声明empty_queue:异常;

declare empty_queue : exception;

在 Modula-3 中,异常是另一种对象,类似于常量、类型、变量或子程序:

In Modula-3, exceptions are another “kind” of object, akin to constants, types, variables, or subroutines:

异常empty_queue;

EXCEPTION empty_queue;

在大多数面向对象语言中,异常是某些预定义或用户定义的类类型的实例:

In most object-oriented languages, an exception is an instance of some predefined or user-defined class type:

类空队列{}; 图片

class empty_queue {};

例 9.39

Example 9.39

参数化异常

Parameterized exceptions

大多数语言允许“参数化”异常,因此引发异常的代码可以将信息传递给处理异常的代码。在面向对象语言中,“参数”只是类的字段:

Most languages allow an exception to be “parameterized,” so the code that raises the exception can pass information to the code that handles it. In object-oriented languages, the “parameters” are simply the fields of the class:

类 duplicate_in_set {      // C++

class duplicate_in_set {     // C++

民众:

public:

 item dup;      // 插入两次的元素

 item dup;     // element that was inserted twice

 duplicate_in_set(项目 d):dup(d){ }

 duplicate_in_set(item d) : dup(d) { }

};

};

抛出 duplicate_in_set(d);

throw duplicate_in_set(d);

在 Modula-3 中,参数包含在异常声明中,就像它们包含在子例程头中一样(示例 9.38中的Modula-3 empty_queue没有参数)。在 Ada 中,标准异常库可用于将信息从raise语句传递到处理程序。如果没有该库,异常只是一个标签,除了名称之外没有其他值。■

In Modula-3, the parameters are included in the exception declaration, much as they are in a subroutine header (the Modula-3 empty_queue in Example 9.38 has no parameters). In Ada, the standard Exceptions library can be used to pass information from a raise statement to a handler. Without the library, an exception is simply a tag, with no value other than its name. ■

如果子程序引发异常但未在内部捕获它,则它可能会以意外的方式“返回”。这种可能性是子程序与程序其余部分接口的重要组成部分。因此,包括 Modula-3、C++ 和 Java 在内的几种语言在每个子程序头中包含一个列表可能传播出子程序的异常。此列表在 Modula-3 中是强制性的:如果发生未在标头中出现且未在内部捕获的异常,则为运行时错误。此列表在 C++ 中是可选的:如果出现,则语义与 Modula-3 中的相同;如果省略,则允许所有异常传播。Java 采用一种折中方法:它将异常分为“已检查”和“未检查”类别。已检查异常必须在子程序标头中声明;未检查异常则不需要。未检查异常通常是大多数程序希望成为致命错误的运行时错误(例如,下标越界)——因此在每个函数中声明它会很麻烦——但如果它们出现在库例程中,高度健壮的程序可能希望捕获它们。

If a subroutine raises an exception but does not catch it internally, it may “return” in an unexpected way. This possibility is an important part of the routine's interface to the rest of the program. Consequently, several languages, including Modula-3, C++, and Java, include in each subroutine header a list of the exceptions that may propagate out of the routine. This list is mandatory in Modula-3: it is a run-time error if an exception arises that does not appear in the header and is not caught internally. The list is optional in C++: if it appears, the semantics are the same as in Modula-3; if it is omitted, all exceptions are permitted to propagate. Java adopts an intermediate approach: it segregates its exceptions into “checked” and “unchecked” categories. Checked exceptions must be declared in subroutine headers; unchecked exceptions need not. Unchecked exceptions are typically run-time errors that most programs will want to be fatal (e.g., subscript out of bounds)—and that would therefore be a nuisance to declare in every function—but that a highly robust program may want to catch if they occur in library routines.

9.4.2 异常传播

9.4.2 Exception Propagation

例 9.40

Example 9.40

C++ 中的多个处理程序

Multiple handlers in C++

在大多数语言中,代码块可以有一个异常处理程序列表。在 C++ 中:

In most languages, a block of code can have a list of exception handlers. In C++:

try {      // 尝试从文件中读取

try {     // try to read from file

 

 

 // 可能很复杂的操作序列

 // potentially complicated sequence of operations

 // 涉及多次调用流 I/O 例程

 // involving many calls to stream I/O routines

 …

 …

} 捕获(文件结束符 e1){

} catch(end_of_file e1) {

 …

 …

} 捕获(io_error e2){

} catch(io_error e2) {

 // 处理除 end_of_file 之外的任何 io_error

 // handler for any io_error other than end_of_file

 …

 …

} 抓住(…) {

} catch(…) {

 // 任何之前未命名的异常的处理程序

 // handler for any exception not previously named

 //(在这种情况下,三点省略号是有效的 C++ 标记;

 // (in this case, the triple-dot ellipsis is a valid C++ token;

 // 它并不表示缺少代码)

 // it does not indicate missing code)

}

}

发生异常时,将按顺序检查处理程序;控制权将转移到与异常匹配的第一个处理程序。在 C++ 中,如果处理程序命名了异常派生自的类,或者它是一个捕获所有异常的程序(…),则该处理程序匹配。在此处的示例中,我们假设end_of_file是io_error的子类。然后,如果发生end_of_file异常,它将由三个catch子句中的第一个处理。所有其他 I/O 错误将由第二个子句捕获;所有非 I/O 错误将由第三个子句捕获。如果缺少最后一个子句,非 I/O 错误将继续在当前子例程中向外传播,然后沿动态链向上传播。■

When an exception arises, the handlers are examined in order; control is transferred to the first one that matches the exception. In C++, a handler matches if it names a class from which the exception is derived, or if it is a catch-all (…). In the example here, let us assume that end_of_file is a subclass of io_error. Then an end_of_file exception, if it arises, will be handled by the first of the three catch clauses. All other I/O errors will be caught by the second; all non-I/O errors will be caught by the third. If the last clause were missing, non-I/O errors would continue to propagate outward in the current subroutine, and then up the dynamic chain. ■

在递归子程序中声明的异常将在运行时被该异常的最内层处理程序捕获。如果异常传播到声明它的范围之外,则处理程序将无法再命名它,因此只能由“全部捕获”处理程序捕获。在具有并发性的语言中,一个必须考虑如果在并发控制线程的最外层未处理异常,将会发生什么情况。在 Modula-3 和 C++ 中,整个程序会异常终止;在 Ada 和 Java 中,受影响的线程会安静地终止;在 C# 中,行为由实现定义。

An exception that is declared in a recursive subroutine will be caught by the innermost handler for that exception at run time. If an exception propagates out of the scope in which it was declared, it can no longer be named by a handler, and thus can be caught only by a “catch-all” handler. In a language with concurrency, one must consider what will happen if an exception is not handled at the outermost level of a concurrent thread of control. In Modula-3 and C++, the entire program terminates abnormally; in Ada and Java, the affected thread terminates quietly; in C#, the behavior is implementation defined.

表达式处理程序

Handlers on Expressions

例 9.41

Example 9.41

OCaml 中的异常处理程序

Exception handler in OCaml

在面向表达式的语言(如 ML 或 Common Lisp)中,异常处理程序附加到表达式,而不是语句。由于处理程序的执行会在发生异常时替换受保护代码的未完成部分,因此附加到表达式的处理程序必须为表达式提供值。(在面向语句的语言中,处理程序(与大多数语句一样)是为了其副作用而执行的。)在 ML 的 OCaml 方言中,处理程序如下所示:

In an expression-oriented language such as ML or Common Lisp, an exception handler is attached to an expression, rather than to a statement. Since execution of the handler replaces the unfinished portion of the protected code when an exception occurs, a handler attached to an expression must provide a value for the expression. (In a statement-oriented language, the handler—like most statements—is executed for its side effects.) In the OCaml dialect of ML, a handler looks like this:

让 foo = 尝试 a / b 与 Division_by_zero -> max_int;;

let foo = try a / b with Division_by_zero -> max_int;;

这里a / b是受保护的表达式,tr y 和with是关键字,Division_by_zero是异常(从异常构造函数构建的值),max_int是一个表达式(在本例中是一个常量),其值替换了出现Division_by_zero异常的表达式的值。受保护的表达式和处理程序通常可以任意复杂,具有许多嵌套函数调用。嵌套调用中出现的异常(并且不在本地处理)会沿动态链向上传播,就像在大多数面向语句的语言中一样。■

Here a / b is the protected expression, try and with are keywords, Division_by_zero is an exception (a value built from the exception constructor), and max_int is an expression (in this case a constant) whose value replaces the value of the expression in which the Division_by_zero exception arose. Both the protected expression and the handler could in general be arbitrarily complicated, with many nested function calls. Exceptions that arise within a nested call (and are not handled locally) propagate back up the dynamic chain, just as they do in most statement-oriented languages. ■

清理作业

Cleanup Operations

在搜索匹配的处理程序的过程中,异常处理机制必须通过回收引发异常的任何子例程的框架来“展开”运行时堆栈。回收框架不仅需要从堆栈中弹出其空间,还需要恢复作为调用序列的一部分保存的任何寄存器。(我们将在第9.4.3 节中更详细地讨论实现问题。)

In the process of searching for a matching handler, the exception-handling mechanism must “unwind” the run-time stack by reclaiming the frames of any subroutines from which the exception escapes. Reclaiming a frame requires not only that its space be popped from the stack but also that any registers that were saved as part of the calling sequence be restored. (We discuss implementation issues in more detail in Section 9.4.3.)

例 9.42

Example 9.42

Python 中的finally子句

finally clause in Python

在 C++ 中,离开范围的异常(无论是子程序还是嵌套块)都需要语言实现来调用析构函数对于在该范围内声明的任何对象。析构函数(将在10.3 节中详细讨论)通常用于释放堆空间和其他资源(例如,打开文件)。Common Lisp 中通过unwind-protect表达式提供了类似的功能,Modula-3、Python、Java 和 C# 中则通过try…finally构造提供了类似的功能。Python 中的代码可能如下所示:

In C++, an exception that leaves a scope, whether a subroutine or just a nested block, requires the language implementation to call destructor functions for any objects declared within that scope. Destructors (to be discussed in more detail in Section 10.3) are often used to deallocate heap space and other resources (e.g., open files). Similar functionality is provided in Common Lisp by an unwind-protect expression, and in Modula-3, Python, Java, and C# by means of try…finally constructs. Code in Python might look like this:

设计与实现

Design & Implementation

9.4 结构化异常

9.4 Structured exceptions

无论从语义还是实用的角度来看,异常处理机制都是现代语言设计中最复杂的方面之一。程序员早在计算机出现之前就已经开始使用子程序(它们出现在 19 世纪 Ada Augusta Byron 伯爵夫人的笔记中)。相比之下,结构化异常直到 20 世纪 70 年代才被发明,直到 20 世纪 80 年代才变得普遍。

Exception-handling mechanisms are among the most complex aspects of modern language design, from both a semantic and a pragmatic point of view. Programmers have used subroutines since before there were computers (they appear, among other places, in the 19th-century notes of Countess Ada Augusta Byron). Structured exceptions, by contrast, were not invented until the 1970s, and did not become commonplace until the 1980s.

尝试:        #受保护的块

try:        # protected block

 my_stream = open(“foo.txt”, “r”)      # “r” 表示“用于阅读”

 my_stream = open(“foo.txt”, “r”)     # “r” means “for reading”

 对于 my_stream 中的行:

 for line in my_stream:

  …

  …

最后:

finally:

 我的流.关闭()

 my_stream.close()

每当控制权从受保护的块中逃逸时,都会执行finally子句,无论逃逸是由于正常完成、退出循环、从当前子例程返回还是传播异常。在我们的示例中,我们假设my_stream在代码开头未绑定到任何内容,并且关闭尚未打开的流是无害的。■

A finally clause will be executed whenever control escapes from the protected block, whether the escape is due to normal completion, an exit from a loop, a return from the current subroutine, or the propagation of an exception. We have assumed in our example that my_stream is not bound to anything at the beginning of the code, and that it is harmless to close a not-yet-opened stream. ■

9.4.3 异常的实现

9.4.3 Implementation of Exceptions

例 9.43

Example 9.43

堆叠异常处理程序

Stacked exception handlers

最明显的异常实现是维护一个处理程序的链接列表堆栈。当控制进入受保护的块时,该块的处理程序将添加到列表的头部。当出现异常时(无论是隐式出现还是作为raisethrow语句的结果出现),语言运行时系统会从列表中弹出最内层的处理程序并调用它。处理程序首先检查它是否与发生的异常匹配;如果不匹配,它只会重新引发它:

The most obvious implementation for exceptions maintains a linked-list stack of handlers. When control enters a protected block, the handler for that block is added to the head of the list. When an exception arises, either implicitly or as a result of a raise or throw statement, the language run-time system pops the innermost handler off the list and calls it. The handler begins by checking to see if it matches the exception that occurred; if not, it simply reraises it:

如果异常与集合中的重复项匹配

if exception matches duplicate in set

 

 

别的

else

 重新引发异常

 reraise exception

为了实现沿动态链向下传播,每个子程序都有一个隐式处理程序,它执行子程序结尾代码的工作,然后重新引发异常。■

To implement propagation back down the dynamic chain, each subroutine has an implicit handler that performs the work of the subroutine epilogue code and then reraises the exception. ■

例 9.44

Example 9.44

每个处理程序有多个异常

Multiple exceptions per handler

如果受保护的代码块具有针对几种不同异常的处理程序,则它们将作为包含多臂if语句的单个处理程序实现:

If a protected block of code has handlers for several different exceptions, they are implemented as a single handler containing a multiarm if statement:

如果异常与文件结尾匹配

if exception matches end of file

 …

 …

elsif 异常匹配 io 错误

elsif exception matches io error

 …

 …

别的

else

 …      ——“catch-all”处理程序 图片

 …     –– “catch-all” handler

这种实现的问题在于,在常见情况下会产生运行时开销。每个受保护的块和每个子例程都以将处理程序推送到处理程序列表的代码开始,并以将其从列表中弹出的代码结束。我们通常可以做得更好。

The problem with this implementation is that it incurs run-time overhead in the common case. Every protected block and every subroutine begins with code to push a handler onto the handler list, and ends with code to pop it back off the list. We can usually do better.

处理程序列表的唯一真正目的是确定哪个处理程序处于活动状态。由于源代码块往往会转换为连续的机器语言指令块,因此我们可以以编译时生成的表的形式捕获处理程序和受保护块之间的对应关系。表中的每个条目包含两个字段:代码块的起始地址和相应处理程序的地址。该表按第一个字段排序。发生异常时,语言运行时系统使用程序计数器作为键在表中执行二进制搜索,以找到当前块的处理程序。如果该处理程序重新引发异常,则重复该过程:处理程序本身是代码块,可以在表中找到。唯一的微妙之处在于与子例程外传播相关的隐式处理程序的情况:这样的处理程序必须确保重新引发的代码使用子例程的返回地址而不是当前程序计数器作为表查找的键。

The only real purpose of the handler list is to determine which handler is active. Since blocks of source code tend to translate into contiguous blocks of machine language instructions, we can capture the correspondence between handlers and protected blocks in the form of a table generated at compile time. Each entry in the table contains two fields: the starting address of a block of code and the address of the corresponding handler. The table is sorted on the first field. When an exception occurs, the language run-time system performs binary search in the table, using the program counter as key, to find the handler for the current block. If that handler reraises the exception, the process repeats: handlers themselves are blocks of code, and can be found in the table. The only subtlety arises in the case of the implicit handlers associated with propagation out of subroutines: such a handler must ensure that the reraise code uses the return address of the subroutine, rather than the current program counter, as the key for table lookup.

在第二种实现中,引发异常的成本更高,是处理程序总数的对数倍。但只有在实际发生异常时才需要支付此成本。假设异常是不寻常的事件,则对性能的净影响显然是有益的:常见情况下的成本为零。在纯粹的形式下,基于表的方法要求编译器能够访问整个程序,或者链接器提供将子表粘合在一起的机制。如果代码片段是独立编译的,我们可以采用混合方法,其中编译器为每个子例程创建一个单独的表,并且每个堆栈框架包含指向相应表的指针。

The cost of raising an exception is higher in this second implementation, by a factor logarithmic in the total number of handlers. But this cost is paid only when an exception actually occurs. Assuming that exceptions are unusual events, the net impact on performance is clearly beneficial: the cost in the common case is zero. In its pure form the table-based approach requires that the compiler have access to the entire program, or that the linker provide a mechanism to glue subtables together. If code fragments are compiled independently, we can employ a hybrid approach in which the compiler creates a separate table for each subroutine, and each stack frame contains a pointer to the appropriate table.

无异常的异常处理

Exception Handling without Exceptions

值得注意的是,有时可以使用不提供内置异常的语言来模拟异常。在第 6.2 节中,我们注意到 Pascal 允许goto到当前子程序之外的标签,Algol 60 允许将标签作为参数传递,而 PL/I 允许将它们存储在变量中。这些机制允许程序以非常非结构化的方式退出深度嵌套的上下文。

It is worth noting that exceptions can sometimes be simulated in a language that does not provide them as a built-in. In Section 6.2 we noted that Pascal permitted gotos to labels outside the current subroutine, that Algol 60 allowed labels to be passed as parameters, and that PL/I allowed them to be stored in variables. These mechanisms permit the program to escape from a deeply nested context, but in a very unstructured way.

在 Scheme 和 Ruby 等语言的call-with-current-continuation (call-cc)例程中可以找到一种更结构化的替代方案。如第 6.2.2 节所述,call-cc接受一个参数f,该参数本身是一个函数。它调用f ,并将一个延续c (闭包)作为参数传递,该延续 c 捕获当前程序计数器和引用环境。在未来的任何时候,f都可以调用c来重新建立保存的环境。如果进行了嵌套调用,控制权会放弃它们,就像处理异常一样。如果我们将受保护的块及其处理程序表示为闭包(lambda 表达式),则call-cc可用于维护一个延续堆栈,应该跳转到该堆栈来模拟raise/throw 。我们将在练习 9.18中进一步探讨这个选项。

A more structured alternative can be found in the call-with-current-continuation (call-cc) routine of languages like Scheme and Ruby. As described in Section 6.2.2, call-cc takes a single argument f, which is itself a function. It calls f, passing as argument a continuation c (a closure) that captures the current program counter and referencing environment. At any point in the future, f can call c to reestablish the saved environment. If nested calls have been made, control abandons them, as it does with exceptions. If we represent a protected block and its handlers as closures (lambda expressions), call-cc can be used to maintain a stack of continuations to which one should jump to emulate raise/throw. We explore this option further in Exercise 9.18.

例 9.45

Example 9.45

C 中的setjmplongjmp

setjmp and longjmp in C

介于非局部goto的混乱和call/cc的普遍性之间,C 提供了一对库例程,名为setjmplongjmp。Setjmp将一个缓冲区作为参数,用于将程序当前状态的表示捕获到该缓冲区中。此缓冲区稍后可以作为第一个参数传递给longjmp 以恢复捕获的状态。对setjmp 的调用返回一个整数:零表示“正常”返回;非零值(作为longjmp的第二个参数提供)表示来自longjmp 的异常“返回” 。典型用法如下

Intermediate between the anarchy of nonlocal gotos and the generality of call/cc, C provides a pair of library routines entitled setjmp and longjmp. Setjmp takes as argument a buffer into which to capture a representation of the program's current state. This buffer can later be passed as the first argument to longjmp, to restore the captured state. Calls to setjmp return an integer: zero indicates “normal” return; nonzero values (provided as the second argument to longjmp) indicate exceptional “returns” from longjmp. Typical uses look like

如果 (!setjmp(缓冲区)) {开关(setjmp(缓冲区)){
 /* 受保护的代码 */ 案例 0:
} 别的 {  /* 受保护的代码 */
 /* 处理程序 */或者  休息;
} 情况 1:
  /* 处理程序 1 */
  休息;
 …
 案例 n:
  /* 处理程序 n */
  休息:
}

最初调用时,setjmp返回 0,控制进入受保护的代码。如果在受保护的代码中的任何地方或在该代码调用的任何内容中调用longjmp(buffer, v) ,则setjmp似乎会再次返回,这次返回值为v,从而导致控制进入处理程序。与call/cc创建的闭包不同, setjmp捕获的信息范围有限:如果包含对setjmp的调用的函数已返回,则longjmp(buffer, v)的行为未定义。■

When initially called, setjmp returns a 0, and control enters the protected code. If longjmp(buffer, v) is called anywhere within the protected code, or in anything called by that code, then setjmp will appear to return again, this time with a return value of v, causing control to enter a handler. Unlike the closure created by call/cc, the information captured by setjmp has limited extent: the behavior of longjmp(buffer, v) is undefined if the function containing the call to setjmp has returned. ■

setjmplongjmp的典型实现将当前机器寄存器保存在setjmp缓冲区中,并在longjmp中恢复它们。没有处理程序列表;实现不是“展开”堆栈,而是通过恢复spfp的旧值来抛出所有嵌套框架。这种方法的问题在于,处理程序开头的寄存器内容不反映受保护代码成功完成部分的效果:它们是在该代码开始运行之前保存的。对已成功完成的变量的任何更改写入内存的所有数据将在处理程序中可见,但缓存在寄存器中的更改将会丢失。为了解决这个限制,C 语言允许程序员将某些变量指定为volatile。 volatile 变量在内存中的值可以“自发”更改,例如由于 I/O 设备活动或并发控制线程而更改。C 语言实现需要在写入 volatile 变量时将其存储到内存中,并在读取它们时从内存加载它们。如果处理程序需要查看可能被受保护代码修改的变量的更改,则程序员必须在变量的声明中包含volatile关键字。

The typical implementation of setjmp and longjmp saves the current machine registers in the setjmp buffer, and restores them in longjmp. There is no list of handlers; rather than “unwinding” the stack, the implementation simply tosses all the nested frames by restoring old values of the sp and fp. The problem with this approach is that the register contents at the beginning of the handler do not reflect the effects of the successfully completed portion of the protected code: they were saved before that code began to run. Any changes to variables that have been written through to memory will be visible in the handler, but changes that were cached in registers will be lost. To address this limitation, C allows the programmer to specify that certain variables are volatile. A volatile variable is one whose value in memory can change “spontaneously,” for example as the result of activity by an I/O device or a concurrent thread of control. C implementations are required to store volatile variables to memory whenever they are written, and to load them from memory whenever they are read. If a handler needs to see changes to a variable that may be modified by the protected code, then the programmer must include the volatile keyword in the variable's declaration.

设计与实现

Design & Implementation

9.5 setjmp

9.5 setjmp

由于 setjmp 将多个寄存器保存到内存中,因此通常的setjmp实现非常昂贵 — 比上述“明显”的异常实现中进入受保护的块更昂贵。虽然实现者可以根据需要自由使用更高效的表驱动方法,但通常的实现可以最大限度地降低运行时系统的复杂性,并且无需从单独编译的模块和库中集成链接器支持的表。

Because it saves multiple registers to memory, the usual implementation of setjmp is quite expensive—more so than entry to a protected block in the “obvious” implementation of exceptions described above. While implementors are free to use a more efficient, table-driven approach if desired, the usual implementation minimizes the complexity of the run-time system and eliminates the need for linker-supported integration of tables from separately compiled modules and libraries.

在09-01-9780124104099检查你的理解

Check Your Understanding

26. 描述一种语言允许程序员声明异常的三种方式。

26. Describe three ways in which a language may allow programmers to declare exceptions.

27. 解释为什么在 C++、Java 和 C# 中将异常定义为类很有用。

27. Explain why it is useful to define exceptions as classes in C++, Java, and C#.

28.解释 try…finally结构的行为和目的。

28. Explain the behavior and purpose of a try…finally construct.

29. 描述在 Ada 或 C++ 等语言中引发异常时用于识别适当处理程序的算法。

29. Describe the algorithm used to identify an appropriate handler when an exception is raised in a language like Ada or C++.

30. 解释如何以在常见情况下(不发生异常时)不产生任何成本的方式实现异常。

30. Explain how to implement exceptions in a way that incurs no cost in the common case (when exceptions don't arise).

31. 像 ML 这样的函数式语言的异常处理程序与像 C++ 这样的命令式语言的异常处理程序有何不同?

31. How do the exception handlers of a functional language like ML differ from those of an imperative language like C++?

32. 描述子程序的隐式处理程序必须执行的操作。

32. Describe the operations that must be performed by the implicit handler for a subroutine.

33.总结C语言 setjmplongjmp库函数的缺点。

33. Summarize the shortcomings of the setjmp and longjmp library routines of C.

34. C 语言中的 volatile变量是什么?它在什么情况下有用?

34. What is a volatile variable in C? Under what circumstances is it useful?

9.5 协程

9.5 Coroutines

了解了运行时堆栈的布局后,我们现在可以考虑更通用的控制抽象的实现——特别是协程。与延续一样,协程由闭包(代码地址和引用环境)表示,我们可以通过非本地 goto 跳转到其中在本例中,这是一种称为传输的特殊操作。这两个抽象之间的主要区别在于,延续是一个常量——一旦创建就不会改变——而协程每次运行时都会改变。当我们转到延续时,旧的程序计数器会丢失,除非我们明确创建一个新的延续来保存它。当我们从一个协程转移到另一个协程时,我们的旧程序计数器已​​保存:我们离开的协程将更新以反映它。因此,如果我们多次执行 goto进入同一个延续,则每次跳转都将从完全相同的位置开始,但如果我们多次执行转移到同一个协程,则每次跳转都将从前一次跳转停止的位置开始。

Given an understanding of the layout of the run-time stack, we can now consider the implementation of more general control abstractions—coroutines in particular. Like a continuation, a coroutine is represented by a closure (a code address and a referencing environment), into which we can jump by means of a nonlocal goto, in this case a special operation known as transfer. The principal difference between the two abstractions is that a continuation is a constant—it does not change once created—while a coroutine changes every time it runs. When we goto a continuation, our old program counter is lost, unless we explicitly create a new continuation to hold it. When we transfer from one coroutine to another, our old program counter is saved: the coroutine we are leaving is updated to reflect it. Thus, if we perform a goto into the same continuation multiple times, each jump will start at precisely the same location, but if we perform a transfer into the same coroutine multiple times, each jump will take up where the previous one left off.

实际上,协程是并发存在的执行上下文,但每次只执行一个,并且通过名称明确地相互转移控制权。协程可用于实现迭代器(第 6.5.3 节)和线程(将在第 13 章中讨论)。它们本身也很有用,特别是对于某些类型的服务器和离散事件模拟。从历史上看,线程最早出现于 Algol 68。今天,它们可以在 Ada、Java、C#、C++、Python、Ruby、Haskell、Go 和 Scala 等许多语言中找到。它们也通常通过库包在语言本身之外提供(尽管语法和语义不那么吸引人)。协程作为用户级编程抽象不太常见。从历史上看,提供它们的两种最重要的语言是 Simula 和 Modula-2。在以下小节中,我们将重点介绍协程的实现以及(在配套站点上)它们在迭代器(第 C-9.5.3 节)和离散事件模拟(第 C-9.5.4 节)中的使用。

In effect, coroutines are execution contexts that exist concurrently, but that execute one at a time, and that transfer control to each other explicitly, by name. Coroutines can be used to implement iterators (Section 6.5.3) and threads (to be discussed in Chapter 13). They are also useful in their own right, particularly for certain kinds of servers, and for discrete event simulation. Threads have appeared, historically, as far back as Algol 68. Today they can be found in Ada, Java, C#, C++, Python, Ruby, Haskell, Go, and Scala, among many others. They are also commonly provided (though with somewhat less attractive syntax and semantics) outside the language proper by means of library packages. Coroutines are less common as a user-level programming abstraction. Historically, the two most important languages to provide them were Simula and Modula-2. We focus in the following subsections on the implementation of coroutines and (on the companion site) on their use in iterators (Section C-9.5.3) and discrete event simulation (Section C-9.5.4).

例 9.46

Example 9.46

显式交错并发计算

Explicit interleaving of concurrent computations

举一个简单的例子,说明协程可能有用的应用程序,假设我们正在编写一个“屏幕保护程序”,该程序在非活动笔记本电脑的屏幕上绘制一幅几乎全黑的图片,并保持图片移动,以避免液晶“烧屏”。再想象一下,我们的屏幕保护程序在后台对文件系统执行“健全性检查”,查找损坏的文件。我们可以按如下方式编写程序:

As a simple example of an application in which coroutines might be useful, imagine that we are writing a “screen saver” program, which paints a mostly black picture on the screen of an inactive laptop, and which keeps the picture moving, to avoid liquid-crystal “burn-in.” Imagine also that our screen saver performs “sanity checks” on the file system in the background, looking for corrupted files. We could write our program as follows:

环形

loop

 –– 更新屏幕上的图片

 –– update picture on screen

 –– 执行下一个健全性检查

 –– perform next sanity check

这种方法的问题在于,连续的健全性检查(以及在较小程度上连续的屏幕更新)可能相互依赖。在大多数系统中,文件系统检查代码具有包含许多循环的深度嵌套控制结构。为了将其分解为可以与屏幕更新交错的部分,程序员必须在每次检查后使用保存嵌套计算状态的代码,并且必须在下一次检查之前使用恢复该状态的代码。■

The problem with this approach is that successive sanity checks (and to a lesser extent successive screen updates) are likely to depend on each other. On most systems, the file-system checking code has a deeply nested control structure containing many loops. To break it into pieces that can be interleaved with the screen updates, the programmer must follow each check with code that saves the state of the nested computation, and must precede the following check with code that restores that state. ■

例 9.47

Example 9.47

交错协程

Interleaving coroutines

一个更有吸引力的方法是将操作转换为协程:5

A much more attractive approach is to cast the operations as coroutines:5

我们,cfs:协同程序

us, cfs : coroutine

协程 check_file_system()

coroutine check_file_system()

 –– 初始化

 –– initialize

 分离

 detach

 对于所有文件

 for all files

  …

  …

  转让(美国)

  transfer(us)

  …

  …

   转让(美国)

   transfer(us)

  …

  …

  转让(美国)

  transfer(us)

  …

  …

协程 update_screen()

coroutine update_screen()

 –– 初始化

 –– initialize

 分离

 detach

 环形

 loop

  …

  …

  转移(cfs)

  transfer(cfs)

  …

  …

开始     –– 主要

begin     –– main

 我们:=新的 update_screen()

 us := new update_screen()

 cfs:=新的check_file_system()

 cfs := new check_file_system()

 转让(美国)

 transfer(us)

此处的语法大致基于 Simula 的语法。首次创建时,协程会执行任何必要的初始化操作,然后将其自身与主程序分离。分离操作会创建一个协程对象,稍后可以将控制权转移到该对象,并将对该协程的引用返回给调用者。转移操作会将当前程序计数器保存在当前协程对象中,并恢复作为参数指定的协程。程序的主体充当初始默认协程的角色。

The syntax here is based loosely on that of Simula. When first created, a coroutine performs any necessary initialization operations, and then detaches itself from the main program. The detach operation creates a coroutine object to which control can later be transfered, and returns a reference to this coroutine to the caller. The transfer operation saves the current program counter in the current coroutine object and resumes the coroutine specified as a parameter. The main body of the program plays the role of an initial, default coroutine.

在check_file_system主体内对transfer的调用可以发生在任意位置,包括嵌套循环和条件。 协同程序也可以调用子程序,就像主程序一样,对transfer的调用可能出现在这些例程内。 执行“下一个”健全性检查所需的上下文以及check_file_system和任何调用例程的局部变量在 transfer 时程序计数器捕获。

Calls to transfer from within the body of check_file_system can occur at arbitrary places, including nested loops and conditionals. A coroutine can also call subroutines, just as the main program can, and calls to transfer may appear inside these routines. The context needed to perform the “next” sanity check is captured by the program counter, together with the local variables of check_file_system and any called routines, at the time of the transfer.

如示例 9.46所示,程序员必须指定何时停止检查文件系统并更新屏幕;协程通过提供transfer操作简化了这项工作,从而无需显式保存和恢复状态。要决定将 transfer 调用放在何处我们必须同时考虑性能和正确性。为了提高性能,我们必须避免在调用之间做太多工作,这样屏幕更新就不会太少。为了保证正确性,我们必须避免在任何检查的中间进行transfer ,因为这可能会因update_screen中的文件访问而受到影响。并行线程(将在第 13 章中描述)将通过确保屏幕更新程序定期接收处理器的份额可以消除第一个问题,但会使第二个问题复杂化:如果两个例程对文件的引用可能会发生干扰,我们需要明确同步这两个例程。■

As in Example 9.46, the programmer must specify when to stop checking the file system and update the screen; coroutines make the job simpler by providing a transfer operation that eliminates the need to save and restore state explicitly. To decide where to place the calls to transfer, we must consider both performance and correctness. For performance, we must avoid doing too much work between calls, so that screen updates aren't too infrequent. For correctness, we must avoid doing a transfer in the middle of any check that might be compromised by file access in update_screen. Parallel threads (to be described in Chapter 13) would eliminate the first of these problems by ensuring that the screen updater receives a share of the processor on a regular basis, but would complicate the second problem: we should need to synchronize the two routines explicitly if their references to files could interfere. ■

设计与实现

Design & Implementation

9.6 线程和协程

9.6 Threads and coroutines

正如我们将在13.2.4 节中看到的那样,有了协程,构建一个简单的线程包很容易。然而,大多数程序员都会同意,线程使用起来要容易得多,因为它们消除了显式传输操作的需要。这种对比——大量额外功能和一点点额外的实现复杂性——可能解释了为什么协程作为一种显式编程抽象相对较少见。

As we shall see in Section 13.2.4, it is easy to build a simple thread package given coroutines. Most programmers would agree, however, that threads are substantially easier to use, because they eliminate the need for explicit transfer operations. This contrast—a lot of extra functionality for a little extra implementation complexity—probably explains why coroutines as an explicit programming abstraction are relatively rare.

9.5.1 堆栈分配

9.5.1 Stack Allocation

由于协程是并发的(即同时启动但未完成),因此协程不能共享单个堆栈:它们的子程序调用和返回作为一个整体,并不是按照后进先出的顺序发生的。 如果每个协程都在词法嵌套的最外层声明(正如 Modula-2 中所要求的那样),那么它们的堆栈是完全不相交的:它们共享的唯一对象是全局的,因此是静态分配的。 大多数操作系统都可以轻松分配一个堆栈,并在执行过程中根据需要增加其虚拟地址空间部分。 分配任意数量的此类堆栈并不容易; 协程的空间在历史上一直是一个实现挑战,至少在虚拟地址空间有限的机器上是这样(64 位架构通过使虚拟地址相对充足,缓解了这个问题)。

Because they are concurrent (i.e., simultaneously started but not completed), coroutines cannot share a single stack: their subroutine calls and returns, taken as a whole, do not occur in last-in-first-out order. If each coroutine is declared at the outermost level of lexical nesting (as was required in Modula-2), then their stacks are entirely disjoint: the only objects they share are global, and thus statically allocated. Most operating systems make it easy to allocate one stack, and to increase its portion of the virtual address space as necessary during execution. It is not as easy to allocate an arbitrary number of such stacks; space for coroutines was historically something of an implementation challenge, at least on machines with limited virtual address space (64-bit architectures ease the problem, by making virtual addresses relatively plentiful).

最简单的方法是给每个协程分配固定数量的静态堆栈空间。这种方法在 Modula-2 中被采用,它要求程序员在初始化协程时指定堆栈的大小和位置。如果协程需要额外的空间,则会出现运行时错误。一些 Modula-2 实现会捕获溢出并暂停并显示错误消息;其他实现则会显示异常行为。如果协程使用的(虚拟)空间少于给定的空间,则多余的空间就浪费了。

The simplest approach is to give each coroutine a fixed amount of statically allocated stack space. This approach was adopted in Modula-2, which required the programmer to specify the size and location of the stack when initializing a coroutine. It was a run-time error for the coroutine to need additional space. Some Modula-2 implementations would catch the overflow and halt with an error message; others would display abnormal behavior. If the coroutine used less (virtual) space than it was given, the excess was simply wasted.

如果像在大多数函数式语言实现中那样从堆中分配堆栈帧,则可以避免溢出和内部碎片问题。同时,每个子例程调用的开销会增加。一个折衷方案是将堆栈分配为较大的固定大小的“块”。每次调用时,子例程调用序列都会检查当前块中是否有足够的空间来容纳被调用例程的帧。如果没有,则分配另一个块并将帧放在那里。每次子例程返回时,结尾代码都会检查当前帧是否是其块中的最后一个。如果是,则将块返回到“空闲块”池。为了减少调用开销,如果编译器能够验证子例程在返回之前不会执行传输,则可以使用普通的中央堆栈 [ Sco91 ]。

If stack frames are allocated from the heap, as they are in most functional language implementations, then the problems of overflow and internal fragmentation are avoided. At the same time, the overhead of each subroutine call increases. An intermediate option is to allocate the stack in large, fixed-size “chunks.” At each call, the subroutine calling sequence checks to see whether there is sufficient space in the current chunk to hold the frame of the called routine. If not, another chunk is allocated and the frame is put there instead. At each subroutine return, the epilogue code checks to see whether the current frame is the last one in its chunk. If so, the chunk is returned to a “free chunk” pool. To reduce the overhead of calls, the compiler can use the ordinary central stack if it is able to verify that a subroutine will not perform a transfer before returning [Sco91].

设计与实现

Design & Implementation

9.7 协程堆栈

9.7 Coroutine stacks

许多语言要求在词汇嵌套的最外层声明协程或线程,以避免非连续堆栈的复杂性。大多数顺序语言的线程库(其中包括 POSIX 标准pthread库)同样要求或至少允许使用连续堆栈。

Many languages require coroutines or threads to be declared at the outermost level of lexical nesting, to avoid the complexity of noncontiguous stacks. Most thread libraries for sequential languages (the POSIX standard pthread library among them) likewise require or at least permit the use of contiguous stacks.

例 9.48

Example 9.48

仙人掌堆

Cactus stacks

如果可以在任意层次的词汇嵌套中创建协程(就像在 Simula 中一样),那么两个或多个协程可能在同一个非全局作用域中声明,因此必须共享对该作用域中对象的访问权限。为了实现这种共享,运行时系统必须使用所谓的仙人掌堆栈(因其与美国西南部的 Saguaro 仙人掌相似而得名;参见图 9.4)。

If coroutines can be created at arbitrary levels of lexical nesting (as they could in Simula), then two or more coroutines maybe declared in the same nonglobal scope, and must thus share access to objects in that scope. To implement this sharing, the run-time system must employ a so-called cactus stack (named for its resemblance to the Saguaro cacti of the American Southwest; see Figure 9.4).

编号:F09-04-9780124104099
图 9.4 仙人掌堆栈。侧面的每个分支代表一个协程(A、B、C 和 D)的创建。右侧显示了块的静态嵌套。静态链接用箭头表示。动态链接简单地通过垂直排列表示:每个例程都调用了其上方的例程。(例如,协程 B 由主程序 M 创建。B 又调用子例程 S 并创建了协程 D。)

堆栈的每个分支都包含一个单独协程的框架。给定协程的动态链在协程开始执行的块中结束。但是,协程的静态链通过任何词汇上围绕的块向下延伸到仙人掌的其余部分。除了 Simula 的协程之外,任何具有词汇嵌套线程的语言的线程都需要仙人掌堆栈。从协程的主块“返回”通常必须终止程序,因为没有迹象表明要转移到哪个例程。由于协程仅在被指定为传输的目标时才会运行因此无需明确终止它。当不再需要给定的协程时,程序员可以简单地重用其堆栈空间,或者在具有垃圾收集功能的语言中,允许收集器自动回收它。■

Each branch off the stack contains the frames of a separate coroutine. The dynamic chain of a given coroutine ends in the block in which the coroutine began execution. The static chain of the coroutine, however, extends down into the remainder of the cactus, through any lexically surrounding blocks. In addition to the coroutines of Simula, cactus stacks are needed for the threads of any language with lexically nested threads. “Returning” from the main block of a coroutine must generally terminate the program, as there is no indication of what routine to transfer to. Because a coroutine only runs when specified as the target of a transfer, there is never any need to terminate it explicitly. When a given coroutine is no longer needed, the programmer can simply reuse its stack space or, in a language with garbage collection, allow the collector to reclaim it automatically. ■

9.5.2 转让

9.5.2 Transfer

要从一个协程转移到另一个协程,运行时系统必须更改程序计数器 (PC)、堆栈和处理器寄存器的内容。这些更改封装在转移操作中:一个协程调用transfer返回一个不同的协程。由于更改发生在transfer内部,因此将 PC 从一个协程更改为另一个协程只需记住正确的返回地址:旧协程从程序中的一个位置调用transfer;新协程返回到一个可能不同的位置。如果transfer将其返回地址保存在堆栈中,则 PC 将作为更改堆栈的副作用自动更改。

To transfer from one coroutine to another, the run-time system must change the program counter (PC), the stack, and the contents of the processor's registers. These changes are encapsulated in the transfer operation: one coroutine calls transfer; a different one returns. Because the change happens inside transfer, changing the PC from one coroutine to another simply amounts to remembering the right return address: the old coroutine calls transfer from one location in the program; the new coroutine returns to a potentially different location. If transfer saves its return address in the stack, then the PC will change automatically as a side effect of changing stacks.

例 9.49

Example 9.49

切换协程

Switching coroutines

那么我们如何更改堆栈?通常的方法是简单地更改堆栈指针寄存器,并避免在传输本身内部使用帧指针。在传输开始时,我们将所有被调用者保存的寄存器以及返回地址(如果子例程调用指令尚未将其推送)推送到当前堆栈上。然后我们更改 sp 从新堆栈中弹出(新)返回地址(ra)和其他寄存器,然后返回:

So how do we change stacks? The usual approach is simply to change the stack pointer register, and to avoid using the frame pointer inside of transfer itself. At the beginning of transfer we push all the callee-saves registers onto the current stack, along with the return address (if it wasn't already pushed by the subroutine call instruction). We then change the sp, pop the (new) return address (ra) and other registers off the new stack, and return:

转移:

transfer:

 推送除 sp 之外的所有寄存器(包括 ra)

 push all registers other than sp (including ra)

 *当前协程:= sp

 *current_coroutine := sp

 current_coroutine := r1      –– 传递给 transfer 的参数

 current_coroutine := r1     –– argument passed to transfer

 sp:=* r1

 sp := *r1

 弹出除 sp 之外的所有寄存器(包括 ra)

 pop all registers other than sp (including ra)

 返回 图片

 return

表示协程或线程的数据结构称为上下文块。在简单的协程包中,上下文块包含单个值:协程的sp(截至其最近一次传输)。(线程包通常会在上下文块中放置其他信息,例如优先级指示或将线程链接到各种调度队列的指针。一些协程或线程包选择将寄存器保存在上下文块中,而不是堆栈顶部;这两种方法都可以。)

The data structure that represents a coroutine or thread is called a context block. In a simple coroutine package, the context block contains a single value: the coroutine's sp as of its most recent transfer. (A thread package generally places additional information in the context block, such as an indication of priority, or pointers to link the thread onto various scheduling queues. Some coroutine or thread packages choose to save registers in the context block, rather than at the top of the stack; either approach works fine.)

在 Modula-2 中,协程创建例程会将协程的堆栈初始化为类似于transfer的框架,并初始化返回地址和寄存器内容以允许“返回”到协程代码的开头。创建例程会将上下文块中的sp值设置为指向这个人工框架,并返回指向上下文块的指针。要开始执行协程,需要将一些现有例程传输到它。

In Modula-2, the coroutine creation routine would initialize the coroutine's stack to look like the frame of transfer, with a return address and register contents initialized to permit a “return” into the beginning of the coroutine's code. The creation routine would set the sp value in the context block to point into this artificial frame, and return a pointer to the context block. To begin execution of the coroutine, some existing routine would need to transfer to it.

在 Simula 中(以及示例 9.47中的代码中),协程创建例程将立即开始执行新的协程,就像它是一个子例程一样。在协程完成任何特定于应用程序的初始化后,它将执行分离操作。分离将设置协程堆栈,使其看起来像transfer的框架,并带有指向以下语句的返回地址。然后,它将允许创建例程返回到其自己的调用者。

In Simula (and in the code in Example 9.47), the coroutine creation routine would begin to execute the new coroutine immediately, as if it were a subroutine. After the coroutine completed any application-specific initialization, it would perform a detach operation. Detach would set up the coroutine stack to look like the frame of transfer, with a return address that pointed to the following statement. It would then allow the creation routine to return to its own caller.

在所有情况下,transfer都需要一个指向上下文块的指针作为参数;通过取消引用该指针,它可以找到下一个要运行的协程的sp 。在示例 9.49的代码中,全局(静态)变量称为current_coroutine,它包含一个指向当前正在运行的协程的上下文块的指针。此指针允许transfer找到应保存旧sp 的位置。

In all cases, transfer expects a pointer to a context block as argument; by dereferencing the pointer it can find the sp of the next coroutine to run. A global (static) variable, called current_coroutine in the code of Example 9.49, contains a pointer to the context block of the currently running coroutine. This pointer allows transfer to find the location in which it should save the old sp.

9.5.3 迭代器的实现

9.5.3 Implementation of Iterators

给定一个协程的实现,迭代器几乎是微不足道的:一个协程用于表示主程序;第二个用于表示迭代器。如果迭代器嵌套,则可能需要额外的协程。

Given an implementation of coroutines, iterators are almost trivial: one coroutine is used to represent the main program; a second is used to represent the iterator. Additional coroutines maybe needed if iterators nest.

在09-02-9780124104099 更深入地

IN MORE DEPTH

更多详细信息请参见配套网站。事实证明,协程对于迭代器实现来说有些过度了。大多数编译器使用两种更简单的替代方案之一。第一种将所有状态保存在单个堆栈中,但有时会在最顶层以外的框架中执行。第二种采用编译时代码转换,以等效迭代器对象透明地替换真正的迭代器。

Additional details appear on the companion site. As it turns out, coroutines are overkill for iterator implementation. Most compilers use one of two simpler alternatives. The first of these keeps all state in a single stack, but sometimes executes in a frame other than the topmost. The second employs a compile-time code transformation to replace true iterators, transparently, with equivalent iterator objects.

9.5.4 离散事件模拟

9.5.4 Discrete Event Simulation

协程的最重要应用之一(也是 Simula 的设计和命名目标)是离散事件模拟。模拟通常指的是创建某个现实世界系统的抽象模型,然后使用该模型进行实验以推断现实世界系统的属性的任何过程。当对现实世界进行实验会变得复杂、危险、昂贵或不切实际时,模拟是可取的。离散事件模拟是一种将模型自然地表达为在特定时间发生的事件(通常是各种有趣对象之间的相互作用)的模拟。离散事件模拟通常不适用于连续过程,例如晶体的生长或水在表面的流动,除非这些过程是在单个粒子的级别捕获的。

One of the most important applications of coroutines (and the one for which Simula was designed and named) is discrete event simulation. Simulation in general refers to any process in which we create an abstract model of some real-world system, and then experiment with the model in order to infer properties of the real-world system. Simulation is desirable when experimentation with the real world would be complicated, dangerous, expensive, or otherwise impractical. A discrete event simulation is one in which the model is naturally expressed in terms of events (typically interactions among various interesting objects) that happen at specific times. Discrete event simulation is usually not appropriate for continuous processes, such as the growth of crystals or the flow of water over a surface, unless these processes are captured at the level of individual particles.

在09-02-9780124104099 更深入地

IN MORE DEPTH

在配套网站上,我们考虑进行交通模拟,其中事件模拟汽车、路口和交通信号灯之间的相互作用。我们为每次驾车出行使用单独的协程。在任何给定时间,我们都会运行具有最早预计到达前方路口时间的协程。我们将不活动的协程保存在按到达时间排序的优先级队列中。

On the companion site we consider a traffic simulation, in which events model interactions among automobiles, intersections, and traffic lights. We use a separate coroutine for each trip to be taken by car. At any given time we run the coroutine with the earliest expected arrival time at an upcoming intersection. We keep inactive coroutines in a priority queue ordered by those arrival times.

9.6 事件

9.6 Events

事件是正在运行的程序(进程)需要响应的事物,但它发生在程序之外,时间不可预测。事件通常由图形用户界面 (GUI) 系统的输入引起:按键鼠标移动、按钮点击。它们也可能是网络操作或其他异步 I/O 活动:消息的到达、先前请求的磁盘操作的完成。

An event is something to which a running program (a process) needs to respond, but which occurs outside the program, at an unpredictable time. Events are commonly caused by inputs to a graphical user interface (GUI) system: keystrokes, mouse motions, button clicks. They may also be network operations or other asynchronous I/O activity: the arrival of a message, the completion of a previously requested disk operation.

在C-8.7 节(尤其是 C-8.7.3 节)讨论的 I/O 操作中,我们假设寻找输入的程序会明确请求输入,如果输入尚不可用,则会等待。这种同步(在指定时间)和阻塞(可能导致等待)输入通常是现代具有图形界面的应用程序所不接受的。相反,程序员通常希望在发生给定事件时调用处理程序(一种特殊的子例程)。处理程序有时也称为回调函数,因为运行时系统会回调主程序,而不是从主程序中调用。在面向对象语言中,回调函数可能是某个处理程序对象的方法,而不是静态子例程。

In the I/O operations discussed in Section C-8.7, and in Section C-8.7.3 in particular, we assumed that a program looking for input will request it explicitly, and will wait if it isn't yet available. This sort of synchronous (at a specified time) and blocking (potentially wait-inducing) input is generally not acceptable for modern applications with graphical interfaces. Instead, the programmer usually wants a handler—a special subroutine—to be invoked when a given event occurs. Handlers are sometimes known as callback functions, because the run-time system calls back into the main program instead of being called from it. In an object-oriented language, the callback function maybe a method of some handler object, rather than a static subroutine.

9.6.1 顺序处理程序

9.6.1 Sequential Handlers

传统上,事件处理程序在顺序编程语言中作为“自发”子例程调用实现,通常使用操作系统在语言本身之外定义和实现的机制。为了准备通过此机制接收事件,程序(称为P )调用setup_handler库例程,将其希望在事件发生时调用的子例程作为参数传递。

Traditionally, event handlers were implemented in sequential programming languages as “spontaneous” subroutine calls, typically using a mechanism defined and implemented by the operating system, outside the language proper. To prepare to receive events through this mechanism, a program—call it P—invokes a setup_handler library routine, passing as argument the subroutine it wants to have invoked when the event occurs.

在硬件层面,P执行期间的异步设备活动将触发中断机制,该机制会保存P的寄存器、切换到不同的堆栈并跳转到 OS 内核中的预定义地址。同样,如果发生中断时其他进程Q正在运行(或者Q本身中的某些操作需要作为事件反映给P),则内核将在其最后一个时间片结束时保存P的状态。无论哪种方式,内核都必须安排调用适当的事件处理程序,尽管P可能位于其代码中通常无法发生子例程调用的位置(例如,它可能位于某个其他子例程的调用序列的中间位置)。

At the hardware level, asynchronous device activity during P's execution will trigger an interrupt mechanism that saves P's registers, switches to a different stack, and jumps to a predefined address in the OS kernel. Similarly, if some other process Q is running when the interrupt occurs (or if some action in Q itself needs to be reflected to P as an event), the kernel will have saved P's state at the end of its last time slice. Either way, the kernel must arrange to invoke the appropriate event handler despite the fact that P may be at a place in its code where a subroutine call cannot normally occur (e.g., it may be halfway through the calling sequence for some other subroutine).

例 9.50

Example 9.50

信号蹦床

Signal trampoline

图 9.5说明了自发子程序调用的典型实现——例如,Unix信号机制所使用的子程序调用。语言运行时库包含一个称为信号蹦床的代码块。它还包括一个可由内核写入、运行时可读取的缓冲区。在传递信号之前,内核将P的保存状态放入共享缓冲区。然后,它切换回P的用户级堆栈并跳转到信号蹦床。蹦床在堆栈中为自己创建一个框架,然后使用正常的子程序调用序列调用事件处理程序。(此机制的正确性取决于中断时堆栈指针寄存器指定的位置之外堆栈中没有重要内容。)当事件处理程序返回时,蹦床从内核写入的缓冲区恢复状态(包括所有寄存器),并跳回主程序。为了避免递归事件,内核通常在跳转时禁用进一步的信号到信号跳床。在跳回原始程序代码之前,跳床会立即执行内核调用以重新启用信号。根据操作系统的详细信息,内核可能会在信号被禁用时缓冲一定数量的信号,并在处理程序重新启用它们后传送它们。■

Figure 9.5 illustrates the typical implementation of spontaneous subroutine calls—as used, for example, by the Unix signal mechanism. The language runtime library contains a block of code known as the signal trampoline. It also includes a buffer writable by the kernel and readable by the runtime. Before delivering a signal, the kernel places the saved state of P into the shared buffer. It then switches back to P's user-level stack and jumps into the signal trampoline. The trampoline creates a frame for itself in the stack and then calls the event handler using the normal subroutine calling sequence. (The correctness of this mechanism depends on there being nothing important in the stack beyond the location specified by the stack pointer register at the time of the interrupt.) When the event handler returns, the trampoline restores state (including all registers) from the buffer written by the kernel, and jumps back into the main program. To avoid recursive events, the kernel typically disables further signals when it jumps to the signal trampoline. Immediately before jumping back to the original program code, the trampoline performs a kernel call to reenable signals. Depending on the details of the operating system, the kernel may buffer some modest number of signals while they are disabled, and deliver them once the handler reenables them. ■

编号09-05-9780124104099
图 9.5 通过 trampoline 传递信号。当发生中断时(或当另一个进程执行应该作为事件出现的操作时),主程序可能位于其代码中的任意位置。内核保存状态并调用trampoline例程,该例程依次通过正常调用序列调用事件处理程序。事件处理程序返回后,trampoline 恢复保存的状态并返回到主程序停止的位置。

实际上,大多数事件处理程序需要与主程序共享数据结构(否则,它们如何让程序对事件做出任何有趣的响应?)。我们必须小心确保处理程序和主程序都不会看到这些共享结构处于不一致状态。具体来说,我们必须防止处理程序在主程序正在修改数据时查看数据,或者在主程序正在读取数据时修改数据。典型的解决方案是通过将主程序中的代码块与禁用和重新启用信号的内核调用括起来,来同步对这些共享结构的访问。我们将在13.2.4 节中使用类似的机制在协程之上实现线程。更一般的同步形式将出现在13.3 节中。

In practice, most event handlers need to share data structures with the main program (otherwise, how would they get the program to do anything interesting in response to the event?). We must take care to make sure neither the handler nor the main program ever sees these shared structures in an inconsistent state. Specifically, we must prevent a handler from looking at data when the main program is halfway through modifying it, or modifying data when the main program is halfway through reading it. The typical solution is to synchronize access to such shared structures by bracketing blocks of code in the main program with kernel calls that disable and reenable signals. We will use a similar mechanism to implement threads on top of coroutines in Section 13.2.4. More general forms of synchronization will appear in Section 13.3.

9.6.2 基于线程的处理程序

9.6.2 Thread-Based Handlers

在现代编程语言和运行时系统中,事件通常由单独的控制线程处理,而不是由自发的子例程调用处理。使用单独的处理程序线程,输入可以再次同步:处理程序线程进行系统调用以请求下一个事件,并等待其发生。同时,主程序继续执行。如果程序希望能够同时处理多个事件,它可以创建多个处理程序线程,每个线程都会调用内核来等待事件。为了保护共享数据结构的完整性,主程序和处理程序线程通常需要一个成熟的同步机制,如第13.3 节所述:禁用信号是不够的。

In modern programming languages and run-time systems, events are often handled by a separate thread of control, rather than by spontaneous subroutine calls. With a separate handler thread, input can again be synchronous: the handler thread makes a system call to request the next event, and waits for it to occur. Meanwhile, the main program continues to execute. If the program wishes to be able to handle multiple events concurrently, it may create multiple handler threads, each of which calls into the kernel to wait for an event. To protect the integrity of shared data structures, the main program and the handler thread(s) will generally require a full-fledged synchronization mechanism, as discussed in Section 13.3: disabling signals will not suffice.

例 9.51

Example 9.51

C# 中的事件处理程序

An event handler in C#

许多现代 GUI 系统都是基于线程的,尽管有些只有一个处理程序线程。示例包括 OpenGL 实用工具包 (GLUT)、GNU 图像处理程序 (GIMP) 工具包 (Gtk)、JavaFX 库和 .NET Windows Presentation Foundation (WPF)。在 C# 中,事件处理程序是委托类型的实例 — 本质上是子例程闭包列表(第 3.6.3 节)。使用 Mono 项目的标准 GUI Gtk#,我们可以按如下方式创建和初始化按钮:

Many contemporary GUI systems are thread-based, though some have just one handler thread. Examples include the OpenGL Utility Toolkit (GLUT), the GNU Image Manipulation Program (GIMP) Tool Kit (Gtk), the JavaFX library, and the .NET Windows Presentation Foundation (WPF). In C#, an event handler is an instance of a delegate type—essentially, a list of subroutine closures (Section 3.6.3). Using Gtk#, the standard GUI for the Mono project, we might create and initialize a button as follows:

void Paused(object sender, EventArgs a) {

void Paused(object sender, EventArgs a) {

 // 按下暂停按钮时执行任何需要执行的操作

 // do whatever needs doing when the pause button is pushed

}

}

按钮 pauseButton = new Button(“暂停”);

Button pauseButton = new Button(“pause”);

暂停按钮。单击 += 新事件处理程序(暂停);

pauseButton.Clicked += new EventHandler(Paused);

ButtonEventHandler在 Gtk# 库中定义。Button是一个代表图形小部件的类。EventHandler是一种委托类型,Paused与之兼容。它的第一个参数表示引发事件的对象;它的第二个参数描述事件本身。Button.Clicked按钮的事件处理程序: EventHandler类型的字段。+= 运算符将新的闭包添加到委托列表中。6图形库安排一个线程调用内核以等待用户界面事件。当我们按下按钮时,调用将从内核返回,线程将调用委托列表上的每个条目。■

Button and EventHandler are defined in the Gtk# library. Button is a class that represents the graphical widget. EventHandler is a delegate type, with which Paused is compatible. Its first argument indicates the object that caused the event; its second argument describes the event itself. Button.Clicked is the button's event handler: a field of EventHandler type. The += operator adds a new closure to the delegate's list.6 The graphics library arranges for a thread to call into the kernel to wait for user interface events. When our button is pushed, the call will return from the kernel, and the thread will invoke each of the entries on the delegate list. ■

例 9.52

Example 9.52

匿名委托处理程序

An anonymous delegate handler

如第 3.6.3 节所述,C# 允许将处理程序更简洁地指定为匿名委托

As described in Section 3.6.3, C# allows the handler to be specified more succinctly as an anonymous delegate:

pauseButton.Clicked += delegate(object sender, EventArgs a) {

pauseButton.Clicked += delegate(object sender, EventArgs a) {

 // 做任何需要做的事情

 // do whatever needs doing

}; 图片

};

例 9.53

Example 9.53

Java 中的事件处理程序

An event handler in Java

其他语言和系统也类似。在 JavaFX 中,事件处理程序通常是实现EventHandler<ActionEvent>接口的类的实例,并带有名为handle 的方法:

Other languages and systems are similar. In JavaFX, an event handler is typically an instance of a class that implements the EventHandler<ActionEvent> interface, with a method named handle:

类 PauseListener 实现 EventHandler<ActionEvent> {

class PauseListener implements EventHandler<ActionEvent> {

 公共无效句柄(ActionEvent e){

 public void handle(ActionEvent e) {

  // 做任何需要做的事情

  // do whatever needs doing

 }

 }

}

}

按钮 pauseButton = new Button();

Button pauseButton = new Button();

暂停按钮.设置文本(“暂停”);

pauseButton.setText(“pause”);

暂停按钮.setOnAction(新的PauseListener()); 图片

pauseButton.setOnAction(new PauseListener());

例 9.54

Example 9.54

匿名内部类处理程序

An anonymous inner class handler

以这种形式编写的语法比 C# 更麻烦。我们可以使用匿名内部类来简化它:

Written in this form, the syntax is more cumbersome than it was in C#. We can simplify it some using an anonymous inner class:

暂停按钮.setOnAction(新 EventHandler<ActionEvent>() {

pauseButton.setOnAction(new EventHandler<ActionEvent>() {

 公共无效句柄(ActionEvent e){

 public void handle(ActionEvent e) {

  // 做任何需要做的事情

  // do whatever needs doing

 }

 }

});

});

这里,我们的PauseListener类的定义嵌入在对new 的调用中(没有名称) ,而后者又嵌入在setOnAction的参数列表中。与 C# 中的匿名委托一样,Java 中的匿名类只能有一个实例。■

Here the definition of our PauseListener class is embedded, without the name, in a call to new, which is in turn embedded in the argument list of setOnAction. Like an anonymous delegate in C#, an anonymous class in Java can have only a single instance. ■

例 9.55

Example 9.55

使用 lambda 表达式处理事件

Handling an event with a lambda expression

我们可以使用 Java 8 lambda 表达式进一步简化语法:

We can simplify the syntax even further by using a Java 8 lambda expression:

暂停按钮.setOnAction(e -> {

pauseButton.setOnAction(e -> {

 // 做任何需要做的事情

 // do whatever needs doing

});

});

此示例利用了Java lambda 表达式的功能接口约定(如示例 3.41中所述) 。使用此约定,我们有效地匹配了 C# 的简洁性。■

This example leverages the functional interface convention of Java lambda expressions, described in Example 3.41. Using this convention, we have effectively matched the brevity of C#. ■

处理程序执行的操作需要简单而简短,以便处理程序线程可以回调内核以处理另一个事件。如果处理程序花费的时间太长,用户可能会发现应用程序没有响应。如果事件需要启动一些计算要求高的操作,或者可能需要执行额外的 I/O,处理程序可能会创建一个新线程来完成这项工作;或者,它可能将请求传递给某个现有的工作线程。

The action performed by a handler needs to be simple and brief, so the handler thread can call back into the kernel for another event. If the handler takes too long, the user is likely to find the application nonresponsive. If an event needs to initiate something that is computationally demanding, or that may need to perform additional I/O, the handler may create a new thread to do the work; alternatively, it may pass a request to some existing worker thread.

在09-01-9780124104099检查你的理解

Check Your Understanding

35. 第一个提供协程的高级编程语言是什么?

35. What was the first high-level programming language to provide coroutines?

36. 协程线程有什么区别?

36. What is the difference between a coroutine and a thread?.

37. 为什么传输库例程在协程之间切换时不需要改变程序计数器?

37. Why doesn't the transfer library routine need to change the program counter when switching between coroutines?

38. 描述三种分配协程堆栈的替代方法。它们的相对优点和缺点是什么?

38. Describe three alternative means of allocating coroutine stacks. What are their relative strengths and weaknesses?

39. 什么是仙人掌堆栈? 它的用途是什么?

39. What is a cactus stack? What is its purpose?

40. 什么是离散事件模拟? 它和协程有什么联系?

40. What is discrete event simulation? What is its connection with coroutines?

41. 从编程语言意义上来说,事件是什么?

41. What is an event in the programming language sense of the word?

42. 总结两种主要的活动实现策略。

42. Summarize the two main implementation strategies for events.

43.解释 匿名委托(C#)和匿名内部类(Java)对处理事件的吸引力。

43. Explain the appeal of anonymous delegates (C#) and anonymous inner classes (Java) for handling events.

9.7 总结和结束语

9.7 Summary and Concluding Remarks

本章重点讨论了控件抽象,特别是子程序。子程序允许程序员将代码封装在一个狭窄的接口后面,然后就可以使用该接口而无需考虑其实现。

This chapter has focused on the subject of control abstraction, and on subroutines in particular. Subroutines allow the programmer to encapsulate code behind a narrow interface, which can then be used without regard to its implementation.

我们从第 9.1 节开始学习子程序,首先回顾了子程序调用堆栈的管理。然后,我们考虑了用于维护堆栈的调用序列,配套站点上有专门介绍显示的额外章节;分别针对 ARM 和 x86 上的 LLVM 和gcc编译器的案例研究;以及SPARC 的寄存器窗口。在简要考虑了内联扩展之后,我们在第 9.3 节中转向参数主题。我们首先考虑了参数传递模式,所有这些模式都是通过传递值、引用或闭包来实现的。我们注意到,语义清晰度和实现速度的目标有时会发生冲突:通过引用传递大参数通常最有效,但由此产生的别名可能会导致程序错误。在第 9.3.3 节中,我们考虑了特殊的参数传递机制,包括默认(可选)参数、命名参数和可变长度参数列表。

We began our study of subroutines in Section 9.1 by reviewing the management of the subroutine call stack. We then considered the calling sequences used to maintain the stack, with extra sections on the companion site devoted to displays; case studies of the LLVM and gcc compilers on ARM and x86, respectively; and the register windows of the SPARC. After a brief consideration of in-line expansion, we turned in Section 9.3 to the subject of parameters. We first considered parameter-passing modes, all of which are implemented by passing values, references, or closures. We noted that the goals of semantic clarity and implementation speed sometimes conflict: it is usually most efficient to pass a large parameter by reference, but the aliasing that results can lead to program bugs. In Section 9.3.3 we considered special parameter-passing mechanisms, including default (optional) parameters, named parameters, and variable-length parameter lists.

在最后三个主要部分中,我们讨论了异常处理机制,它允许程序以结构良好的方式从嵌套的子程序调用序列中“解开”;协程,它允许程序维护(并在两个或多个执行上下文之间切换);以及事件,它允许程序响应异步外部活动。在配套网站上,我们解释了如何使用协程进行离散事件模拟。我们还注意到它们可用于实现迭代器,但这里有更简单的替代方案。在第 13 章中,我们将基于协程来实现线程,这些线程彼此并行运行(或看起来并行运行)。

In the final three major sections we considered exception-handling mechanisms, which allow a program to “unwind” in a well-structured way from a nested sequence of subroutine calls; coroutines, which allow a program to maintain (and switch between) two or more execution contexts; and events, which allow a program to respond to asynchronous external activity. On the companion site we explained how coroutines are used for discrete event simulation. We also noted that they could be used to implement iterators, but here simpler alternatives exist. In Chapter 13, we will build on coroutines to implement threads, which run (or appear to run) in parallel with one another.

在几种情况下,我们可以看出,关于语言应该提供哪些类型的控制抽象,人们的共识正在不断演变。Fortran 和 Algol 60 等语言的有限参数传递模式已被更广泛或更灵活的选项所取代。几种语言使用默认和命名参数增强了参数的标准位置表示法。结构化程度较低的错误处理机制(如标签参数、非本地goto和动态绑定处理程序)已被结构化异常处理程序所取代,这些异常处理程序在子例程中具有词法作用域,在常见(无异常)情况下可以以零成本实现。传统信号处理机制的自发子例程调用已被专用线程中的回调所取代。在许多情况下,实现这些新功能需要编译器和运行时系统变得更加复杂。有时,如按名称调用参数、标签参数或非本地goto的情况,语义上令人困惑的功能也难以实现,放弃它们使编译器变得更简单。在其他情况下,有用但难以实现的语言特性继续出现在某些语言中,但在其他语言中却没有出现。此类示例包括一级子程序、协同程序、迭代器、延续和具有无限范围的本地对象。

In several cases we can discern an evolving consensus about the sorts of control abstractions that a language should provide. The limited parameter-passing modes of languages like Fortran and Algol 60 have been replaced by more extensive or flexible options. Several languages augment the standard positional notation for arguments with default and named parameters. Less-structured error-handling mechanisms, such as label parameters, nonlocal gotos, and dynamically bound handlers, have been replaced by structured exception handlers that are lexically scoped within subroutines, and can be implemented at zero cost in the common (no-exception) case. The spontaneous subroutine call of traditional signal-handling mechanisms have been replaced by callbacks in a dedicated thread. In many cases, implementing these newer features has required that compilers and run-time systems become more complex. Occasionally, as in the case of call-by-name parameters, label parameters, or nonlocal gotos, features that were semantically confusing were also difficult to implement, and abandoning them has made compilers simpler. In yet other cases language features that are useful but difficult to implement continue to appear in some languages but not in others. Examples in this category include first-class subroutines, coroutines, iterators, continuations, and local objects with unlimited extent.

9.8 练习

9.8 Exercises

9.1 尽可能多地描述命令式编程语言中的函数与数学中的函数的不同之处。

9.1 Describe as many ways as you can in which functions in imperative programming languages differ from functions in mathematics.

9.2 考虑以下 C++ 代码:class string_map { string cached_key; string cached_val; const string complex_lookup(const string key); // body specified anywhere public: const string operator[](const string key) { if (key == cached_key) return cached_val; string rtn_val = complex_lookup(key); cached_key = key; cached_val = rtn_val; return rtn_val; } };假设string_map::operator []包含程序中对complex_lookup的唯一调用。解释为什么程序员以文本方式内联扩展该调用并消除单独的函数是不明智的。

 

  

  

  

   

 

  

   

   

   

   

   

  

 

9.2 Consider the following code in C++:

 class string_map {

  string cached_key;

  string cached_val;

  const string complex_lookup(const string key);

   // body specified elsewhere

 public:

  const string operator[](const string key) {

   if (key == cached_key) return cached_val;

   string rtn_val = complex_lookup(key);

   cached_key = key;

   cached_val = rtn_val;

   return rtn_val;

  }

 };

Suppose that string_map::operator [] contains the only call to complex_lookup anywhere in the program. Explain why it would be unwise for the programmer to expand that call textually in-line and eliminate the separate function.

9.3 使用您最喜欢的语言和编译器,编写一个程序,可以告知某些子程序参数的评估顺序。

9.3 Using your favorite language and compiler, write a program that can tell the order in which certain subroutine parameters are evaluated.

9.4 考虑以下 C 语言中的(错误)程序:void foo() { int i; printf(“%d “, i++); } int main() { int j; for (j = 1; j <= 10; j++) foo(); }子程序foo中的局部变量i从未初始化。然而,在许多系统上,该程序将显示可重复的行为,打印0 1 2 3 4 5 6 7 8 9。请提出解释。同时解释为什么其他系统上的行为可能不同或不确定。

 

  

  

 

 

  

  

 

9.4 Consider the following (erroneous) program in C:

 void foo() {

  int i;

  printf(“%d “, i++);

 }

 int main() {

  int j;

  for (j = 1; j <= 10; j++) foo();

 }

Local variable i in subroutine foo is never initialized. On many systems, however, the program will display repeatable behavior, printing 0 1 2 3 4 5 6 7 8 9. Suggest an explanation. Also explain why the behavior on other systems might be different, or nondeterministic.

9.5 1980 年 Digital VAX 指令 的标准调用序列不仅使用堆栈指针 ( sp ) 和帧指针 ( fp ),还使用单独的参数指针( ap )。在什么情况下,这个单独的指针会很有用?换句话说,什么时候不必将参数放置在距 fp 的静态已知偏移量处会很方便

9.5 The standard calling sequence for the c. 1980 Digital VAX instruction set employed not only a stack pointer (sp) and frame pointer (fp), but a separate arguments pointer (ap) as well. Under what circumstances might this separate pointer be useful? In other words, when might it be handy not to have to place arguments at statically known offsets from the fp?

9.6 编写(用您选择的语言)一个过程或函数,它将具有四种不同的效果,具体取决于参数是通过值、通过引用、通过值/结果还是通过名称传递。

9.6 Write (in the language of your choice) a procedure or function that will have four different effects, depending on whether arguments are passed by value, by reference, by value/result, or by name.

9.7 考虑在 Fortran 中传递给子程序的表达式a + b。将此表达式作为对未命名临时变量的引用传递(如 Fortran 所做的那样)与按值传递(例如在 Pascal 中)之间是否存在语义上有意义的区别?也就是说,程序员能否分辨出作为值的参数和作为对临时变量的引用的参数之间的区别?

9.7 Consider an expression like a + b that is passed to a subroutine in Fortran. Is there any semantically meaningful difference between passing this expression as a reference to an unnamed temporary (as Fortran does) or passing it by value (as one might, for example, in Pascal)? That is, can the programmer tell the difference between a parameter that is a value and a parameter that is a reference to a temporary?

9.8 考虑 Fortran 77 中的以下子程序:subroutine shift(a, b, c) integer a, b, c a = b b = c end假设我们想调用shift(x, y, 0)但不想改变y的值。由于知道构建的表达式是作为临时变量传递的,我们决定调用shift(x, y+0, 0)。我们的代码一开始运行良好,但是当我们启用优化时(使用某些编译器)会失败。这是怎么回事?我们可以做什么呢?

 

 

 

 

 

9.8 Consider the following subroutine in Fortran 77:

 subroutine shift(a, b, c)

 integer a, b, c

 a = b

 b = c

 end

Suppose we want to call shift(x, y, 0) but we don't want to change the value of y. Knowing that built-up expressions are passed as temporaries, we decide to call shift(x, y+0, 0). Our code works fine at first, but then (with some compilers) fails when we enable optimization. What is going on? What might we do instead?

9.9 在 Fortran IV 的某些实现中,以下代码将打印 3。您能给出解释吗?您认为较新的 Fortran 实现如何解决这个问题?c主程序调用 foo(2) print*, 2 stop end subroutine foo(x) x = x + 1 return end

    

    

    

    

    

    

      

      

    

9.9 In some implementations of Fortran IV, the following code would print a 3. Can you suggest an explanation? How do you suppose more recent Fortran implementations get around the problem?

 c   main program

    call foo(2)

    print*, 2

    stop

    end

    subroutine foo(x)

      x = x + 1

      return

    end

9.10 假设你正在编写一个程序,其中所有参数都必须按名称传递。你能编写一个子程序来交换其实际参数的值吗?请解释。(提示:考虑相互依赖的参数,如iA[i]。)

9.10 Suppose you are writing a program in which all parameters must be passed by name. Can you write a subroutine that will swap the values of its actual parameters? Explain. (Hint: Consider mutually dependent parameters like i and A[i].)

9.11 你能用Java 或其他任何只带有共享调用参数的语言编写swap例程吗?在这样的语言中swap到底应 该做什么?(提示:考虑变量引用的对象与该对象的值 [内容] 之间的区别。)

9.11 Can you write a swap routine in Java, or in any other language with only call-by-sharing parameters? What exactly should swap do in such a language? (Hint: Think about the distinction between the object to which a variable refers and the value [contents] of that object.)

9.12如 第 9.3.1 节所述, Ada 83 中的输出参数可由调用方写入但不能读取。在 Ada 95 中,它们既可读取又可写入,但它们的生命周期开始时未初始化。您认为 Ada 95 的设计者为什么要进行这种改变?这样做有什么缺点吗?

9.12 As noted in Section 9.3.1, out parameters in Ada 83 can be written by the callee but not read. In Ada 95 they can be both read and written, but they begin their life uninitialized. Why do you think the designers of Ada 95 made this change? Does it have any drawbacks?

9.13  Swift 借鉴了 Ada 的思路,提供了一种inout参数模式。语言手册没有指定inout参数是通过引用传递还是通过值结果传递。编写一个程序来确定本地 Swift 编译器使用的实现。

9.13 Taking a cue from Ada, Swift provides an inout parameter mode. The language manual does not specify whether inout parameters are to be passed by reference or value-result. Write a program that determines the implementation used by your local Swift compiler.

9.14在 Pascal 中, 压缩记录的字段(示例 8.8)不能通过引用传递。同样,在通过引用传递子范围变量时,Pascal 要求相应形式参数的所有可能值对该子范围都有效:type small = 1..100; R = record x, y : small; end; S = packed record x, y : small; end; var a : 1..10; b : 1..1000; c : R; d : S; procedure foo(var n : small); begin n := 100; writeln(a); end; a := 2; foo(b); (* ok *) foo(a); (* 静态语义错误 *) foo(cx); (* ok *) foo(dx); (* 静态语义错误 *)

 

   

   

    

   

   

   

 

 

   

   

 

 

 

    

    

    

    

利用你所学到的参数传递模式,解释这些语言限制。

9.14 Fields of packed records (Example 8.8) cannot be passed by reference in Pascal. Likewise, when passing a subrange variable by reference, Pascal requires that all possible values of the corresponding formal parameter be valid for the subrange:

 type small = 1..100;

   R = record x, y : small; end;

   S = packed record x, y : small; end;

 var   a : 1..10;

   b : 1..1000;

   c : R;

   d : S;

 procedure foo(var n : small);

 begin

   n := 100;

   writeln(a);

 end;

 

 a := 2;

 foo(b);   (* ok *)

 foo(a);   (* static semantic error *)

 foo(c.x);   (* ok *)

 foo(d.x);   (* static semantic error *)

Using what you have learned about parameter-passing modes, explain these language restrictions.

9.15 考虑 C 语言中的以下声明:double(*foo(double (*)(double, double[]), double)) (double, …);用英语描述foo的类型

 

9.15 Consider the following declaration in C:

 double(*foo(double (*)(double, double[]), double)) (double, …);

Describe in English the type of foo.

9.16 当程序员在子程序调用中省略可选参数时,程序运行速度会更快吗?为什么或为什么不?

9.16 Does a program run faster when the programmer leaves optional parameters out of a subroutine call? Why or why not?

9.17 你认为为什么高级编程语言很少支持可变长度参数列表?

9.17 Why do you suppose that variable-length argument lists are so seldom supported by high-level programming languages?

9.18在 练习 6.35的基础上,说明如何在 Scheme 中使用call-with-current-continuation实现异常。以Common Lisp 的handler-case为范本,建立语法模型。与练习 6.35一样,您可能需要define-syntaxdynamic-wind

9.18 Building on Exercise 6.35, show how to implement exceptions using call-with-current-continuation in Scheme. Model your syntax after the handler-case of Common Lisp. As in Exercise 6.35, you will probably need define-syntax and dynamic-wind.

9.19 鉴于你所学到的关于结构化异常实现的知识,描述如何实现 Pascal 的非局部goto或 Algol 60 的标签参数(第 6.2 节)。你是否需要对这些功能的使用方式进行任何限制?

9.19 Given what you have learned about the implementation of structured exceptions, describe how you might implement the nonlocal gotos of Pascal or the label parameters of Algol 60 (Section 6.2). Do you need to place any restrictions on how these features can be used?

9.20描述 C++ 析构函数或 Java  try…finally块的合理实现。编译器必须在程序的什么位置生成哪些代码,以确保在离开作用域时始终进行清理?

9.20 Describe a plausible implementation of C++ destructors or Java try… finally blocks. What code must the compiler generate, at what points in the program, to ensure that cleanup always occurs when leaving a scope?

9.21 使用线程在 Java 中构建对真正迭代器的支持。尝试将尽可能多的实现隐藏在合理的接口后面。特别是,隐藏名为yield的例程(由迭代器调用)和标准 Java Iterator接口例程(在循环体中调用)的实现中对new thread、thread.start、thread.join、waitnotify的任何使用。将迭代器的性能与内置迭代器对象的性能进行比较(结果可能不太好)。讨论您在语言的抽象功能中遇到的任何弱点。

9.21 Use threads to build support for true iterators in Java. Try to hide as much of the implementation as possible behind a reasonable interface. In particular, hide any uses of new thread, thread.start, thread.join, wait, and notify inside implementations of routines named yield (to be called by an iterator) and in the standard Java Iterator interface routines (to be called in the body of a loop). Compare the performance of your iterators to that of the built-in iterator objects (it probably won't be good). Discuss any weaknesses you encounter in the abstraction facilities of the language.

9.22 在 Common Lisp 中,多级返回使用catchthrow;大多数其他现代语言风格的异常处理使用handler-caseerror。说明它们之间的区别主要在于风格,而不是表达能力。换句话说,说明每种功能都可以用来模拟另一种功能。

9.22 In Common Lisp, multilevel returns use catch and throw; exception handling in the style of most other modern languages uses handler-case and error. Show that the distinction between these is mainly a matter of style, rather than expressive power. In other words, show that each facility can be used to emulate the other.

9.23 编译并运行图 9.6中的程序。解释其行为。创建一个行为更可预测的新版本。

9.23 Compile and run the program in Figure 9.6. Explain its behavior. Create a new version that behaves more predictably.

编号:F09-06-9780124104099
图 9.6 一个有问题的 C 程序,用于说明信号的使用。在大多数 Unix 系统中,SIGTSTP信号是通过在键盘上键入 control-Z 来生成的。

9.24 使用 C#、Java 或其他具有基于线程的事件处理的语言,围绕示例 9.519.54的“暂停按钮”构建一个简单的程序。您的程序应打开一个小窗口,其中包含一个文本字段和两个按钮,一个标记为“暂停”,另一个标记为“恢复”。然后它应在文本字段中显示一个整数,从零开始,每秒计数一次。如果按下暂停按钮,计数应暂停;如果按下恢复按钮,计数应继续。

注意​​,您的程序至少需要两个线程 - 一个用于计数,一个用于处理事件。在 Java 中,JavaFX 包将自动创建处理程序线程,然后您的主程序可以进行计数。在 C# 中,某个现有线程需要调用Application.Run才能成为处理程序线程。在这种情况下,您需要第二个线程来进行计数。

9.24 In C#, Java, or some other language with thread-based event handling, build a simple program around the “pause button” of Examples 9.519.54. Your program should open a small window containing a text field and two buttons, one labeled “pause”, the other labeled “resume”. It should then display an integer in the text field, starting with zero and counting up once per second. If the pause button is pressed, the count should suspend; if the resume button is pressed, it should continue.

Note that your program will need at least two threads—one to do the counting, one to handle events. In Java, the JavaFX package will create the handler thread automatically, and your main program can do the counting. In C#, some existing thread will need to call Application.Run in order to become a handler thread. In this case you'll need a second thread to do the counting.

9.25 通过添加“克隆”按钮来扩展您对上一个问题的答案。按下此按钮应创建一个包含另一个计数器的附加窗口。当然,这将需要额外的线程。

9.25 Extend your answer to the previous problem by adding a “clone” button. Pushing this button should create an additional window containing another counter. This will, of course, require additional threads.

在09-02-9780124104099 9.26–9.36 更深入。

9.26–9.36  In More Depth.

9.9 探索

9.9 Explorations

9.37 探索 GNU Ada 翻译器gnat中子程序调用的细节。特别注意更复杂的语言特性,包括嵌套块中的声明(第 3.3.2 节)、动态大小数组(第 8.2.2 节)、输入/输出参数(第 9.3.1 节)、可选和命名参数(第 9.3.3 节)、通用子程序(第 7.3.1 节)、异常(第 9.4 节)和并发(“启动时细化”,第 13.2.3 节)。

9.37 Explore the details of subroutine calls in the GNU Ada translator gnat. Pay particular attention to the more complex language features, including declarations in nested blocks (Section 3.3.2), dynamic-size arrays (Section 8.2.2), in/out parameters (Section 9.3.1), optional and named parameters (Section 9.3.3), generic subroutines (Section 7.3.1), exceptions (Section 9.4), and concurrency (“Launch-at-Elaboration,” Section 13.2.3).

9.38 如果你正在设计一种新的命令式语言,你会选择哪组参数模式?为什么?

9.38 If you were designing a new imperative language, what set of parameter modes would you pick? Why?

9.39 了解 PHP 中的引用和引用赋值运算符。讨论它们与 C++ 引用的相同点和不同点。特别要注意的是,PHP 中的赋值可以改变引用变量所引用的对象。为什么 PHP 允许这样做而 C++ 不允许?

9.39 Learn about references and the reference assignment operator in PHP. Discuss the similarities and differences between these and the references of C++. In particular, note that assignments in PHP can change the object to which a reference variable refers. Why does PHP allow this but C++ does not?

9.40 了解 C++ 中的方法指针。它们有什么用处?它们与封装方法的 C# 委托有何不同?

9.40 Learn about pointers to methods in C++. What are they useful for? How do they differ from a C# delegate that encapsulates a method?

9.41 查找几种有异常的语言的手册,并查找预定义异常集(即语言实现可能自动引发的异常集)。讨论不同语言定义的异常集之间的差异。如果你正在设计一个异常处理工具,你会预定义哪些异常(如果有的话)?为什么?

9.41 Find manuals for several languages with exceptions and look up the set of predefined exceptions—those that may be raised automatically by the language implementation. Discuss the differences among the sets defined by different languages. If you were designing an exception-handling facility, what exceptions, if any, would you make predefined? Why?

9.42  Eiffel 是“替代模型”异常处理的一个例外。其救援子句表面上类似于catch块,但它必须重试它所附加的例程,或者允许异常沿调用链向上传播。换句话说,当控制权从救援子句的末尾脱落时,默认行为是重新引发异常。阅读“契约式设计”,这是此异常处理机制支持的编程方法。您是否同意反对替代的论点?解释一下。

9.42 Eiffel is an exception to the “replacement model” of exception handling. Its rescue clause is superficially similar to a catch block, but it must either retry the routine to which it is attached or allow the exception to propagate up the call chain. Put another way, the default behavior when control falls off the end of the rescue clause is to reraise the exception. Read up on “Design by Contract,” the programming methodology supported by this exception-handling mechanism. Do you agree or disagree with the argument against replacement? Explain.

9.43 学习 Common Lisp 中非本地控制传输的细节。编写一个教程来解释tagbodygoblockreturn-fromcatchthrow;以及restart-case、restart-bind、handler-case、handler-bind、find-restart、invoke-restart、ignore-errors、signalerror。你觉得所有这些机制怎么样?是不是有点矫枉过正?一定要给出一个例子来说明handler-bind的用法。

9.43 Learn the details of nonlocal control transfer in Common Lisp. Write a tutorial that explains tagbody and go; block and return-from; catch and throw; and restart-case, restart-bind, handler-case, handler-bind, find-restart, invoke-restart, ignore-errors, signal, and error. What do you think of all this machinery? Is it over-kill? Be sure to give an example that illustrates the use of handler-bind.

9.44对于 Common Lisp、Modula-3 和 Java,比较 unwind-protecttry…finally的语义。具体来说,如果在 cleanup 子句中出现异常会发生什么?

9.44 For Common Lisp, Modula-3, and Java, compare the semantics of unwind-protect and try…finally. Specifically, what happens if an exception arises within a cleanup clause?

9.45如 第 9.6.2 节末尾所述,事件处理程序需要快速执行或将其工作传递给另一个线程。在asyncawait原语中可以找到后者的一个特别优雅的机制C# 5 和 F# 中类似的asynclet!。阅读这些promitives支持的异步编程模型。解释它们与迭代器的(实现级)连接(第 C-9.5.3 节)。编写一个基于 GUI 的程序或网络服务器,充分利用它们。

9.45 As noted near the end of Section 9.6.2, an event-handler needs either to execute quickly or to pass its work off to another thread. A particularly elegant mechanism for the latter can be found in the async and await primitives of C# 5 and the similar async and let! of F#. Read up on the asynchronous programming model supported by these promitives. Explain their (implementation-level) connection to iterators (Section C-9.5.3). Write a GUI-based program or a network server that makes good use of them.

9.46 比较和对比几种 GUI 系统的事件处理机制。处理程序如何与事件绑定?你能控制它们的调用顺序吗?每个系统支持多少个事件处理线程?处理程序线程是如何以及何时创建的?它们如何与程序的其余部分同步?

9.46 Compare and contrast the event-handling mechanisms of several GUI systems. How are handlers bound to events? Can you control the order in which they are invoked? How many event-handling threads does each system support? How and when are handler threads created? How do they synchronize with the rest of the program?

在09-02-9780124104099 9.47–9.52 更深入。

9.47–9.52  In More Depth.

9.10 书目注释

9.10 Bibliographic Notes

递归子程序最初通过 McCarthy 在 Lisp 上的工作而为人所知 [ McC60 ]。7递归子程序的基于堆栈的空间管理是使用 Algol 60 编译器开发的(例如,参见 Randell 和 Russell [ RR64 ])。(由于范围问题,Lisp 中的子程序空间需要更通用的基于堆的分配。)Dijkstra [ Dij60 ] 介绍了使用显示访问非本地数据的早期讨论。Hanson [ Han81 ] 认为嵌套子程序是不必要的。

Recursive subroutines became known primarily through McCarthy's work on Lisp [McC60].7 Stack-based space management for recursive subroutines developed with compilers for Algol 60 (see, e.g., Randell and Russell [RR64]). (Because of issues of extent, subroutine space in Lisp requires more general, heap-based allocation.) Dijkstra [Dij60] presents an early discussion of the use of displays to access nonlocal data. Hanson [Han81] argues that nested subroutines are unnecessary.

gcc的调用序列和堆栈约定部分记录在随编译器分发的texinfo文件中(请参阅www.gnu.org/software/gcc)。LLVM 的文档可在 llvm.org 找到。配套站点上描述的几个细节是通过检查两个编译器的输出“逆向工程”的。

Calling sequences and stack conventions for gcc are partially documented in the texinfo files distributed with the compiler (see www.gnu.org/software/gcc). Documentation for LLVM can be found at llvm.org. Several of the details described on the companion site were “reverse engineered” by examining the output of the two compilers.

Ada 语言基本原理 [ IBFW91第 8 章] 包含对参数传递模式的出色讨论。Harbison [ Har92,第 6.2-6.3 节]描述了 Modula-3 模式并将其与其他语言的模式进行了比较。Liskov 和 Guttag [ LG86,第 25 页] 将 Clu 中的共享调用比作 Lisp 中的参数传递。按名称调用参数源于 Alonzo Church [ Chu41 ] 的 lambda 演算,我们将在 C-11.7.1 节中对其进行更详细的介绍。Thunk 最早由 Ingerman [ Ing61 ]描述。Fleck [ Fle76 ] 讨论了尝试编写带有按名称调用参数的交换例程时遇到的问题(练习 9.10)。

The Ada language rationale [IBFW91, Chap. 8] contains an excellent discussion of parameter-passing modes. Harbison [Har92, Secs. 6.2–6.3] describes the Modula-3 modes and compares them to those of other languages. Liskov and Guttag [LG86, p. 25] liken call-by-sharing in Clu to parameter passing in Lisp. Call-by-name parameters have their roots in the lambda calculus of Alonzo Church [Chu41], which we consider in more detail in Section C-11.7.1. Thunks were first described by Ingerman [Ing61]. Fleck [Fle76] discusses the problems involved in trying to write a swap routine with call-by-name parameters (Exercise 9.10).

MacLaren [ Mac77 ] 描述了 PL/I 中的异常处理。Ada 和大多数较新的语言的词法作用域替代方案大量借鉴了 Goodenough [ Goo75 ]的工作。Luckam 和 Polak [ LP80 ]正式描述了 Ada 的语义。Clu 的异常是一个有趣的历史先驱;详细信息可以在 Liskov 和 Snyder 的著作 [ LS79 ] 中找到。Meyer [ Mey92a ] 讨论了 Eiffel 中的契约式设计和异常处理。Friedman、Wand 和 Haynes [ FWH01第 8-9 章] 对 Scheme 中的延续传递风格进行了出色的解释。

MacLaren [Mac77] describes exception handling in PL/I. The lexically scoped alternative of Ada, and of most more recent languages, draws heavily on the work of Goodenough [Goo75]. Ada's semantics are described formally by Luckam and Polak [LP80]. Clu's exceptions are an interesting historical precursor; details can be found in the work of Liskov and Snyder [LS79]. Meyer [Mey92a] discusses Design by Contract and exception handling in Eiffel. Friedman, Wand, and Haynes [FWH01, Chaps. 8-9] provide an excellent explanation of continuation-passing style in Scheme.

Conway [ Con63 ]的作品中出现了对协程的早期描述,他用协程来表示编译的各个阶段。Birtwistle 等人 [ BDMN73 ] 在 Simula 67 中提供了使用协程进行模拟的教程介绍。Cactus 堆栈至少可以追溯到 20 世纪 60 年代中期;Burroughs B6500 和 B7500 计算机 [ HD68 ] 在硬件上直接支持它们。Murer 等人 [ MOSS96 ] 讨论了 Sather 编程语言(Eiffel 的后代)中迭代器的实现。Von Behren 等人 [ vCZ + 03 ] 描述了一个基于块的堆栈分配系统。

An early description of coroutines appears in the work of Conway [Con63], who used them to represent the phases of compilation. Birtwistle et al. [BDMN73] provide a tutorial introduction to the use of coroutines for simulation in Simula 67. Cactus stacks date from at least the mid-1960s; they were supported directly in hardware by the Burroughs B6500 and B7500 computers [HD68]. Murer et al. [MOSS96] discuss the implementation of iterators in the Sather programming language (a descendant of Eiffel). Von Behren et al. [vCZ+03] describe a system with chunk-based stack allocation.


1控制抽象和数据抽象之间的区别有些模糊,因为后者通常不仅封装信息,还封装访问和修改信息的操作。换句话说,大多数数据抽象都包含控制抽象。

1 The distinction between control and data abstraction is somewhat fuzzy, because the latter usually encapsulates not only information but also the operations that access and modify that information. Put another way, most data abstractions include control abstraction.

2叶例程之所以如此命名,是因为它是子例程调用图(练习 3.10中提到的数据结构)的叶子。

2 A leaf routine is so named because it is a leaf of the subroutine call graph, a data structure mentioned in Exercise 3.10.

3按照解析 C 声明的通常规则(示例 8.46中的脚注), r是指向huge_record的指针,其值为常量。如果我们希望r成为指向huge_record的常量,则需要说huge_record* const r

3 Following the usual rules for parsing C declarations (footnote in Example 8.46), r is a pointer to a huge_record whose value is constant. If we wanted r to be a constant that points to a huge_record, we should need to say huge_record* const r.

4实际情况实际上要复杂一些:整数的 put 例程嵌套在integer_IO中,而 integer_IO 是一个通用包,它又位于text_IO中。程序员必须为整数类型的每种类型(大小)实例化一个单独的integer_IO包版本

4 The real situation is actually a bit more complicated: The put routine for integers is nested inside integer_IO, a generic package that is in turn inside of text_IO. The programmer must instantiate a separate version of the integer_IO package for each variety (size) of integer type.

在本例中也可以使用5 个线程,实际上可能更能满足我们的需求。协程就足够了,因为执行上下文数量较少(即两个),而且很容易确定一个上下文应该转移到另一个上下文的点。

5 Threads could also be used in this example, and might in fact serve our needs a bit better. Coroutines suffice because there is a small number of execution contexts (namely two), and because it is easy to identify points at which one should transfer to the other.

6从技术上讲, Clicked属于事件 EventHandler类型。事件修饰符使委托私有化,因此只能从声明它的类中调用它。同时,它创建一个公共属性,其中包含addremove 访问器方法。这些允许类外的代码向事件添加处理程序(使用+=)并从事件中删除它们(使用−=)。

6 Technically, Clicked is of event EventHandler type. The event modifier makes the delegate private, so it can be invoked only from within the class in which it was declared. At the same time, it creates a public property, with add and remove accessor methods. These allow code outside the class to add handlers to the event (with +=) and remove them from it (with −=).

7约翰·麦卡锡(1927-2011),曾长期担任麻省理工学院和斯坦福大学的教授,是人工智能领域的创始人之一。他于 1958 年推出了 Lisp,并为分时技术的早期发展以及使用数学逻辑推理计算机程序做出了重要贡献。他于 1971 年获得 ACM 图灵奖。

7 John McCarthy (1927–2011), long-time Professor at MIT and then Stanford Universities, was one of the founders of the field of Artificial Intelligence. He introduced Lisp in 1958, and also made key contributions to the early development of time-sharing and the use of mathematical logic to reason about computer programs. He received the ACM Turing Award in 1971.

10

数据抽象和面向对象

Data Abstraction and Object Orientation

第 3 章中 我们介绍了数据抽象发展的几个阶段,重点介绍了控制名称可见性的作用域机制。我们从全局变量开始,其生存期跨越程序执行。然后,我们添加了局部变量,其生存期仅限于单个子例程的执行;嵌套作用域,允许子例程本身是局部的;以及静态变量,其生存期跨越执行,但其名称仅在单个作用域内可见。接下来是模块,允许一组子例程共享一组静态变量;模块类型,允许程序员实例化给定抽象的多个实例,以及,允许程序员定义相关抽象的系列。

In Chapter 3 we presented several stages in the development of data abstraction, with an emphasis on the scoping mechanisms that control the visibility of names. We began with global variables, whose lifetime spans program execution. We then added local variables, whose lifetime is limited to the execution of a single subroutine; nested scopes, which allow subroutines themselves to be local; and static variables, whose lifetime spans execution, but whose names are visible only within a single scope. These were followed by modules, which allow a collection of subroutines to share a set of static variables; module types, which allow the programmer to instantiate multiple instances of a given abstraction, and classes, which allow the programmer to define families of related abstractions.

普通模块鼓励“管理器”风格的编程,其中模块导出抽象类型。模块类型和类允许模块本身成为抽象类型。区别在两个方面显而易见。首先,通常从管理器模块导出的显式创建销毁例程被模块类型实例的创建和销毁所取代。其次,调用特定模块实例中的例程取代了调用需要导出类型的变量作为参数的通用例程。类在模块即类型方法的基础上添加了继承机制,允许将新抽象定义为对现有抽象的改进或扩展,以及动态方法绑定,允许抽象的新版本显示新改进的行为,即使在需要早期版本的上下文中使用也是如此。类的实例称为对象基于类的语言和编程技术被称为面向对象。1

Ordinary modules encourage a “manager” style of programming, in which a module exports an abstract type. Module types and classes allow the module itself to be the abstract type. The distinction becomes apparent in two ways. First, the explicit create and destroy routines typically exported from a manager module are replaced by creation and destruction of an instance of the module type. Second, invocation of a routine in a particular module instance replaces invocation of a general routine that expects a variable of the exported type as argument. Classes build on the module-as-type approach by adding mechanisms for inheritance, which allows new abstractions to be defined as refinements or extensions to existing ones, and dynamic method binding, which allows a new version of an abstraction to display newly refined behavior, even when used in a context that expects an earlier version. An instance of a class is known as an object; languages and programming techniques based on classes are said to be object-oriented.1

第 3 章中介绍的数据抽象机制的逐步演化是一种组织思想的有用方法,但它并不能完全反映语言特性的历史发展。特别是,认为面向对象编程是模块的产物是不准确的。相反,面向对象编程的所有三个基本概念(封装、继承和动态方法绑定)都源于 Simula 编程语言,该语言由挪威计算中心的 Ole-Johan Dahl 和 Kristen Nygaard 于 20 世纪 60 年代中期开发。2现代面向对象语言相比,Simula 在封装的数据隐藏部分较弱,而 Clu、Modula、Euclid 和相关语言在 20 世纪 70 年代正是在这一领域做出了重要贡献。与此同时,继承和动态方法绑定的思想在 20 世纪 70 年代被 Smalltalk 采纳和完善。

The stepwise evolution of data abstraction mechanisms presented in Chapter 3 is a useful way to organize ideas, but it does not completely reflect the historical development of language features. In particular, it would be inaccurate to suggest that object-oriented programming developed as an outgrowth of modules. Rather, all three of the fundamental concepts of object-oriented programming—encapsulation, inheritance, and dynamic method binding—have their roots in the Simula programming language, developed in the mid-1960s by Ole-Johan Dahl and Kristen Nygaard of the Norwegian Computing Center.2 In comparison to modern object-oriented languages, Simula was weak in the data hiding part of encapsulation, and it was in this area that Clu, Modula, Euclid, and related languages made important contributions in the 1970s. At the same time, the ideas of inheritance and dynamic method binding were adopted and refined in Smalltalk over the course of the 1970s.

Smalltalk 采用独特的“基于消息”的编程模型,具有动态类型和不寻常的术语和语法。动态类型往往会使实现相对较慢,并延迟错误报告。该语言还紧密集成到图形编程环境中,因此很难跨系统移植。由于这些原因,考虑到 Smalltalk 对后续开发的影响,其使用范围没有人们想象的那么广泛。Eiffel、C++、Ada 95、Fortran 2003、Java 和 C# 等语言在很大程度上代表了 Smalltalk 的继承和动态方法绑定与“主流”命令式语法和语义的重新整合。另一方面,Objective-C 以相对纯粹和纯粹的形式将 Smalltalk 风格的消息传递和动态类型与用于对象内操作的传统 C 语法相结合。面向对象在函数式语言中也变得很重要,例如 Common Lisp 对象系统 (CLOS [ Kee89Ste90,第 28 章]) 和 OCaml 的对象。

Smalltalk employed a distinctive “message-based” programming model, with dynamic typing and unusual terminology and syntax. The dynamic typing tended to make implementations relatively slow, and delayed the reporting of errors. The language was also tightly integrated into a graphical programming environment, making it difficult to port across systems. For these reasons, Smalltalk was less widely used than one might have expected, given the influence it had on subsequent developments. Languages like Eiffel, C++, Ada 95, Fortran 2003, Java, and C# represented to a large extent a reintegration of the inheritance and dynamic method binding of Smalltalk with “mainstream” imperative syntax and semantics. In an alternative vein, Objective-C combined Smalltalk-style messaging and dynamic typing, in a relatively pure and unadulterated form, with traditional C syntax for intra-object operations. Object orientation has also become important in functional languages, as exemplified by the Common Lisp Object System (CLOS [Kee89; Ste90, Chap. 28]) and the objects of OCaml.

最近,动态类型对象在 Python 和 Ruby 等语言中获得了新的流行度,而静态类型对象则继续出现在 Scala 和 Go 等语言中。Swift 是 Objective-C 的后继者,它遵循其前身(实际上也是 OCaml)的模式,将动态类型对象分层放在静态类型语言之上。

More recently, dynamically typed objects have gained new popularity in languages like Python and Ruby, while statically typed objects continue to appear in languages like Scala and Go. Swift, the successor to Objective-C, follows the pattern of its predecessor (and of OCaml, in fact) in layering dynamically typed objects on top of an otherwise statically typed language.

10.1 节中,我们概述了面向对象编程及其三个基本概念。在10.2 节中,我们将更详细地讨论封装和数据隐藏。然后,我们在10.3 节中讨论对象初始化和终止化,在10.4 节中讨论动态方法绑定。在10.6 节(主要在配套站点上)中,我们讨论多重继承的主题,其中一个类是根据多个现有类定义的。我们将看到,多重继承带来了一些特别棘手的语义和实现挑战。最后,在第 10.7 节中,我们重新审视面向对象的定义,考虑一种语言能够或应该在多大程度上建模一切都是对象。我们的讨论主要集中在 Smalltalk、Eiffel、C++ 和 Java 上,尽管我们也将有机会提到许多其他语言。我们将在第14.4.4 节中回到动态类型对象的主题。

In Section 10.1 we provide an overview of object-oriented programming and of its three fundamental concepts. We consider encapsulation and data hiding in more detail in Section 10.2. We then consider object initialization and finalizationin Section 10.3, and dynamic method binding in Section 10.4. In Section 10.6 (mostly on the companion site) we consider the subject of multiple inheritance, in which a class is defined in terms of more than one existing class. As we shall see, multiple inheritance introduces some particularly thorny semantic and implementation challenges. Finally, in Section 10.7, we revisit the definition of object orientation, considering the extent to which a language can or should model everything as an object. Most of our discussion will focus on Smalltalk, Eiffel, C++, and Java, though we shall have occasion to mention many other languages as well. We will return to the subject of dynamically typed objects in Section 14.4.4.

10.1 面向对象编程

10.1 Object-Oriented Programming

例 10.1

Example 10.1

C++ 中的list_node类

list_node class in C++

面向对象编程可以看作是一种尝试,通过使定义新抽象为现有抽象的扩展细化变得容易,从而增强代码重用的机会。作为示例的起点,考虑一个整数集合,它被实现为一个双向链表(我们将在第 10.1.1 节中考虑其他类型对象的集合)。图 10.1包含集合元素的 C++ 代码。该示例采用了“模块即类型”风格的抽象:每个元素都是list_node类的单独对象。该类包含数据成员prevnexthead_nodeval)和子程序成员predecessorinsert_beforeremove )。在许多面向对象语言中,子程序成员称为方法;数据成员也称为字段。C ++的关键字this 指的是当前执行的方法所属的对象。在 Smalltalk 和 Objective-C 中,等效关键字是self;在 Eiffel 中它是current。■

Object-oriented programming can be seen as an attempt to enhance opportunities for code reuse by making it easy to define new abstractions as extensions or refinements of existing abstractions. As a starting point for examples, consider a collection of integers, implemented as a doubly linked list of records (we'll consider collections of other types of objects in Section 10.1.1). Figure 10.1 contains C++ code for the elements of our collection. The example employs a “module-as-type” style of abstraction: each element is a separate object of class list_node. The class contains both data members (prev, next, head_node, and val) and subroutine members (predecessor, successor, insert_before and remove). Subroutine members are called methods in many object-oriented languages; data members are also called fields. The keyword this in C++ refers to the object of which the currently executing method is a member. In Smalltalk and Objective-C, the equivalent keyword is self; in Eiffel it is current. ■

f10-01-9780124104099
图 10.1 C++ 中列表节点的简单类。在此示例中,我们设想一个整数列表。

例 10.2

Example 10.2

使用list_node的列表类

list class that uses list_node

鉴于list_node类的存在,我们可以定义一个整数列表,如下所示:

Given the existence of the list_node class, we could define a list of integers as follows:

班级列表 {

class list {

 list_node 标题;

 list_node header;

民众:

public:

 // 不需要显式的构造函数;

 // no explicit constructor required;

 // 隐式构造“header”就足够了

 // implicit construction of 'header' suffices

 int 空() {

 int empty() {

  返回 header.singleton();

  return header.singleton();

 }

 }

 列表节点 * 头() {

 list_node* head() {

  返回 header.successor();

  return header.successor();

 }

 }

 无效附加(list_node * new_node){

 void append(list_node *new_node) {

  header.插入(新节点);

  header.insert_before(new_node);

 }

 }

 ~list() {           // 析构函数

 ~list() {          // destructor

  如果(!header.singleton())

  if (!header.singleton())

   抛出新的 list_err(“尝试删除非空列表”);

   throw new list_err(“attempt to delete nonempty list”);

 }

 }

};

};

要创建一个空列表,可以这样写

To create an empty list, one could then write

列表* my_list_ptr = 新列表;

list* my_list_ptr = new list;

插入到列表中的记录的创建方式大致相同:

Records to be inserted into a list are created in much the same way:

列表节点* elem_ptr = 新列表节点

list_node* elem_ptr = new list_node;

例 10.3

Example 10.3

内联(扩展)对象的声明

Declaration of in-line (expanded) objects

在 C++ 中,也可以简单地声明给定类的对象:

In C++, one can also simply declare an object of a given class:

列出我的列表;

listmy_list;

列表节点元素;

list_node elem;

我们的清单 类包含这样的对象 ( header ) 作为字段。使用new创建时,对象会分配在堆中;通过声明创建时,对象会静态分配或在堆栈中,具体取决于生存期(Eiffel 称此类对象为“扩展”)。无论是在堆栈还是在堆中,对象创建都会导致调用程序员指定的初始化例程,称为构造函数在 C++ 及其后代 Java 和 C# 中,构造函数的名称与类本身的名称相同。C++ 还允许程序员指定析构函数方法,当对象被销毁时,该方法将自动调用,无论是通过明确的程序员操作还是从声明它的子例程返回。析构函数的名称也与类的名称相同,但以波浪号 ( ~ ) 开头。析构函数通常用于存储管理和错误检查。■

Our list class includes such an object (header) as a field. When created with new, an object is allocated in the heap; when created via elaboration of a declaration it is allocated statically or on the stack, depending on lifetime (Eiffel calls such objects “expanded”). Whether on the stack or in the heap, object creation causes the invocation of a programmer-specified initialization routine, known as a constructor. In C++ and its descendants, Java and C#, the name of the constructor is the same as that of the class itself. C++ also allows the programmer to specify a destructor method that will be invoked automatically when an object is destroyed, either by explicit programmer action or by return from the subroutine in which it was declared. The destructor's name is also the same as that of the class, but with a leading tilde (~). Destructors are commonly used for storage management and error checking. ■

例 10.4

Example 10.4

构造函数参数

Constructor arguments

如果构造函数有参数,则在声明内联对象或在堆中创建对象时必须提供相应的参数。例如,假设我们的list_node构造函数被编写为采用显式参数:

If a constructor has parameters, corresponding arguments must be provided when declaring an in-line object or creating an object in the heap. Suppose, for example, that our list_node constructor had been written to take an explicit parameter:

类列表节点{

class list_node {

 …

 …

 列表节点(int v){

 list_node(int v) {

  上一个 = 下一个 = head_node = 这个;

  prev = next = head_node = this;

  瓦尔=v;

  val = v;

 }

 }

每个内联声明或对new 的调用都需要提供一个值:

Each in-line declaration or call to new would then need to provide a value:

list_node element1(0);      // 内联

list_node element1(0);     // in-line

list_node *e_ptr = new list_node(13);    // 堆

list_node *e_ptr = new list_node(13);   // heap

正如我们将在10.3.1节中看到的,C++实际上允许我们声明两个构造函数,并使用函数重载的通常规则来区分它们:没有值的声明将调用无参数构造函数;带有整数参数的声明将调用整数参数构造函数。■

As we shall see in Section 10.3.1, C++ actually allows us to declare both constructors, and uses the usual rules of function overloading to differentiate between them: declarations without a value will call the no-parameter constructor; declarations with an integer argument will call the integer-parameter constructor. ■

公共和私有成员

Public and Private Members

公众 list_node声明中的标签将抽象实现所需的成员与抽象用户可用的成员分开。 按照3.3.4 节的术语,出现在public标签之后的成员将从类中导出;而出现在标签之前的成员则不会导出。 C++ 还提供了private标签,因此可以根据需要首先列出类的公开可见部分(甚至混合列出)。 在许多其他语言中,公共数据和子例程成员(字段和方法)必须单独标记(有关更多信息,请参见10.2.2 节)。 请注意,C++ 类是开放作用域,如3.3.4 节所定义;不需要显式导入任何内容。

The public label within the declaration of list_node separates members required by the implementation of the abstraction from members available to users of the abstraction. In the terminology of Section 3.3.4, members that appear after the public label are exported from the class; members that appear before the label are not. C++ also provides a private label, so the publicly visible portions of a class can be listed first if desired (or even intermixed). In many other languages, public data and subroutine members (fields and methods) must be individually so labeled (more on this in Section 10.2.2). Note that C++ classes are open scopes, as defined in Section 3.3.4; nothing needs to be explicitly imported.

例 10.5

Example 10.5

方法声明无定义

Method declaration without definition

在许多语言中(包括 C++),某些信息可以不包含在模块或类的初始声明中,而是放在抽象用户不可见的单独文件中。在我们的运行示例中,我们可以声明list_node的公共方法而不提供其主体:

In many languages—C++ among them—certain information can be left out of the initial declaration of a module or class, and provided in a separate file not visible to users of the abstraction. In our running example, we could declare the public methods of list_node without providing their bodies:

类列表节点{

class list_node {

 列表节点* 上一个;

 list_node* prev;

 列表节点*下一个;

 list_node* next;

 列表节点*头节点;

 list_node* head_node;

民众:

public:

 int 值;

 int val;

 列表节点();

 list_node();

 list_node* 前任();

 list_node* predecessor();

 list_node* 后继();

 list_node* successor();

 bool 单例();

 bool singleton();

 无效插入前(list_node * new_node);

 void insert_before(list_node* new_node);

 无效删除();

 void remove();

 ~列表节点();

 ~list_node();

};

};

设计与实现

Design & Implementation

10.1 类声明中包括什么?

10.1 What goes in a class declaration?

两个规则决定了在类声明中而不是在单独的定义中放置什么。首先,声明必须包含程序员正确使用抽象所需的所有信息。其次,声明必须包含编译器生成代码所需的所有信息。第二条规则通常更广泛:它倾向于将第一条规则不需要的信息强制放入接口(的私有部分),特别是在使用变量值模型而不是引用模型的语言中。如果编译器必须生成代码来分配空间(例如,在堆栈框架中)来保存类的实例,那么它必须知道该实例的大小;这是在类声明中包含私有字段的理由。此外,如果编译器要以内联方式扩展任何方法调用,那么它必须提供它们的代码。以内联方式扩展面向对象程序中最小、最常用的方法往往对获得良好的性能至关重要。

Two rules govern the choice of what to put in the declaration of a class, rather than in a separate definition. First, the declaration must contain all the information that a programmer needs in order to use the abstraction correctly. Second, the declaration must contain all the information that the compiler needs in order to generate code. The second rule is generally broader: it tends to force information that is not required by the first rule into (the private part of) the interface, particularly in languages that use a value model of variables, instead of a reference model. If the compiler must generate code to allocate space (e.g., in stack frames) to hold an instance of a class, then it must know the size of that instance; this is the rationale for including private fields in the class declaration. In addition, if the compiler is to expand any method calls inline then it must have their code available. In-line expansion of the smallest, most common methods of an object-oriented program tends to be crucial for good performance.

例 10.6

Example 10.6

单独的方法定义

Separate method definition

然后可以将这个略微简化的类声明放在.h “头”文件中,而将方法主体放在.cc “实现”文件中。(C++ 单独编译的约定与 C 的约定类似,我们在C-3.8 节中看到过。此处使用的文件名后缀是 GNU g++编译器所期望的。)在.cc文件中,方法定义的头必须使用 ::作用域解析运算符来标识它所属的类:

This somewhat abbreviated class declaration might then be put in a .h “header” file, with method bodies relegated to a .cc “implementation” file. (C++ conventions for separate compilation are similar to those of C, which we saw in Section C-3.8. The file name suffixes used here are those expected by the GNU g++ compiler.) Within a .cc file, the header of a method definition must identify the class to which it belongs by using a :: scope resolution operator:

void list_node::insert_before(list_node* new_node) {

void list_node::insert_before(list_node* new_node) {

 如果(!new_node->singleton())

 if (!new_node->singleton())

  抛出新的list_err(“尝试插入列表中已有的节点”);

  throw new list_err(“attempt to insert node already on list”);

 上一个->下一个 = 新节点;

 prev->next = new_node;

 新节点->上一个 = 上一个;

 new_node->prev = prev;

 new_node->next = this;

 new_node->next = this;

 上一个=新节点;

 prev = new_node;

 新节点->头节点 = 头节点;

 new_node->head_node = head_node;

}

}

微型子程序

Tiny Subroutines

面向对象程序往往比普通命令式程序调用更多的子程序,而且子程序往往更短。在冯·诺依曼语言中,许多通过直接访问记录字段可以完成的事情往往隐藏在面向对象语言的对象方法中。事实上,许多程序员认为声明公共字段是一种不好的做法,因为这样做会让抽象的用户直接访问内部表示,并且使得在不更改用户代码的情况下无法更改该表示。可以说,我们应该将list_nodeval字段设为私有,并使用get_valset_val方法来读取和写入它。

Object-oriented programs tend to make many more subroutine calls than do ordinary imperative programs, and the subroutines tend to be shorter. Lots of things that would be accomplished by direct access to record fields in a von Neumann language tend to be hidden inside object methods in an object-oriented language. Many programmers in fact consider it bad style to declare public fields, because doing so gives users of an abstraction direct access to the internal representation, and makes it impossible to change that representation without changing the user code as well. Arguably, we should make the val field of list_node private, with get_val and set_val methods to read and write it.

例 10.7

Example 10.7

C# 中的属性索引器方法

property and indexer methods in C#

C# 提供了一种专门设计用于简化方法(称为访问器)声明以“获取”和“设置”私有字段的属性机制。使用此机制,我们的val字段的 C# 版本可以编写如下:

C# provides a property mechanism specifically designed to facilitate the declaration of methods (called accessors) to “get” and “set” private fields. Using this mechanism, a C# version of our val field could be written as follows:

类列表节点{

class list_node {

 …

 …

 int val;           // val(小写 'v')是私有的

 int val;          // val (lower case 'v') is private

 公共 int Val {

 public int Val {

  get {           // 存在 get 访问器和可选

  get {          // presence of get accessor and optional

   返回 val;           // 设置访问器意味着 Val 是一个属性

   return val;          // set accessor means that Val is a property

  }

  }

  放 {

  set {

   val = value;           // value 是一个关键字:要设置的参数

   val = value;          // value is a keyword: argument to set

  }

  }

 }

 }

 …

 …

}

}

list_node类的用户现在可以通过(公共)Val 属性访问(私有)val 字段就像是一个字段一样:

Users of the list_node class can now access the (private) val field through the (public) Val property as if it were a field:

列表节点n;

list_node n;

int a = n.Val;      // 隐式调用 get 方法

int a = n.Val;     // implicit call to get method

n.Val = 3;      // 隐式调用 set 方法

n.Val = 3;     // implicit call to set method

实际上,C# 索引器提供了直接字段访问的外观(从类的用户角度来看),同时保留了更改实现的能力。类似的索引器机制可以使任意类的对象看起来像数组,在左值和右值上下文中都使用常规下标语法。示例见边栏 8.3。

In effect, C# indexers provide the look of direct field access (from the perspective of a class's users) while preserving the ability to change the implementation. A similar indexer mechanism can make objects of arbitrary classes look like arrays, with conventional subscript syntax in both l-value and r-value contexts. An example appears in Sidebar 8.3.

在C++ 中,运算符重载和引用可用于提供与索引器等效的功能,但不能提供与属性等效的功能。■

In C++, operator overloading and references can be used to provide the equivalent of indexers, but not of properties. ■

派生类

Derived Classes

例 10.8

Example 10.8

列表派生的队列类

queue class derived from list

现在假设我们已经有一个列表抽象,并且想要一个队列抽象。我们可以从头开始定义队列,但大部分代码看起来与图 10.1中相同。在面向对象语言中,我们可以从列表中派生队列,允许它继承预先存在的字段和方法:

Suppose now that we already have a list abstraction, and would like a queue abstraction. We could define the queue from scratch, but much of the code would look the same as in Figure 10.1. In an object-oriented language we have the alternative of deriving the queue from the list, allowing it to inherit preexisting fields and methods:

classqueue:publiclist{      //队列源自列表

class queue : public list {     // queue is derived from list

民众:

public:

 // 不需要专门的构造函数或析构函数

 // no specialized constructor or destructor required

 无效入队(int v){

 void enqueue(int v) {

  append(new list_node(v));      // append 继承自 list

  append(new list_node(v));     // append is inherited from list

 }

 }

 int 出队() {

 int dequeue() {

  如果 (空())

  if (empty())

   抛出新的 list_err(“尝试从空队列中出队”);

   throw new list_err(“attempt to dequeue from empty queue”);

  list_node* p = head();      // head 也是继承的

  list_node* p = head();     // head is also inherited

  p->删除();

  p->remove();

  int v = p->val;

  int v = p->val;

  删除p;

  delete p;

  返回v;

  return v;

 }

 }

};

};

这里,队列被称为派生类(也称为子类子类);列表被称为基类(也称为父类超类)。派生类自动继承基类的所有字段和方法。程序员需要显式声明的是队列具有列表缺少的成员——在本例中为入队出队方法。我们很快就会看到派生类也具有额外字段的示例。■

Here queue is said to be a derived class (also called a child class or subclass); list is said to be a base class (also called a parent class or superclass). The derived class inherits all the fields and methods of the base class, automatically. All the programmer needs to declare explicitly are members that a queue has but a list lacks—in this case, the enqueue and dequeue methods. We shall see examples shortly in which derived classes have extra fields as well. ■

例 10.9

Example 10.9

隐藏基类的成员

Hiding members of a base class

在 C++ 中,基类的公共成员在派生类的方法内部始终可见。仅当派生类声明的第一行中基类名称前面带有关键字public时,它们才对派生类的用户可见。当然,我们可能并不总是希望这些成员可见。在我们的队列示例中,我们选择在enqueuedequeue之间传递整数,并在内部分配和释放list_nodes。如果我们想让这些列表节点保持隐藏,我们必须阻止用户访问类list的headappend方法。我们可以通过将list设为私有基类来实现这一点:

In C++, public members of a base class are always visible inside the methods of a derived class. They are visible to users of the derived class only if the base class name is preceded with the keyword public in the first line of the derived class's declaration. Of course, we may not always want these members to be visible. In our queue example, we have chosen to pass integers to and from enqueue and dequeue, and to allocate and deallocate the list_nodes internally. If we want to keep these list nodes hidden, we must prevent the user from accessing the head and append methods of class list. We can do so by making list a private base class instead:

类队列:私有列表{…

class queue : private list { …

为了使空方法再次可见,我们可以明确调用它:

To make the empty method visible again, we can call it out explicitly:

民众:

public:

 使用列表::empty;

 using list::empty;

我们将在10.2.2节详细讨论类成员的可见性。■

We will discuss the visibility of class members in more detail in Section 10.2.2. ■

在 C++ 中创建派生类的对象时,编译器会首先调用基类的构造函数,然后再调用派生类的构造函数。在我们的队列示例中,如果派生类没有构造函数,则仍会调用列表构造函数 — 这当然是我们想要的。我们将在第 10.3 节进一步讨论构造函数。

When an object of a derived class is created in C++, the compiler arranges to call the constructor for the base class first, and then to call the constructor of the derived class. In our queue example, where the derived class lacks a constructor, the list constructor will still be called—which is, of course, what we want. We will discuss constructors further in Section 10.3.

通过从旧类派生新类,程序员可以创建任意深度的类层次结构,并在树的每一层上添加附加功能。 Smalltalk 和 Java 的标准库分别有七层和八层之深。 (与 C++ 不同, Smalltalk 和 Java 都有一个根超类Object,所有其他类都从该超类派生而来。 C#、Objective-C 和 Eiffel 有一个类似的类; Eiffel 称之为ANY。)

By deriving new classes from old ones, the programmer can create arbitrarily deep class hierarchies, with additional functionality at every level of the tree. The standard libraries for Smalltalk and Java are as many as seven and eight levels deep, respectively. (Unlike C++, both Smalltalk and Java have a single root superclass, Object, from which all other classes are derived. C#, Objective-C, and Eiffel have a similar class; Eiffel calls it ANY.)

修改基类方法

Modifying Base Class Methods

例 10.10

Example 10.10

在派生类中重新定义方法

Redefining a method in a derived class

除了定义新的字段和方法,并隐藏不再希望可见的字段和方法之外,派生类还可以通过提供新版本来重新定义基类的成员。在我们的队列示例中,我们可能希望定义一个新的head方法,用于“查看”队列的第一个元素,但不将其删除:

In addition to defining new fields and methods, and hiding those it no longer wants to be visible, a derived class can redefine a member of a base class simply by providing a new version. In our queue example, we might want to define a new head method that “peeks” at the first element of the queue, without removing it:

类队列:私有列表{

class queue : private list {

 …

 …

 int 头() {

 int head() {

  如果 (空())

  if (empty())

   抛出新的 list_err(“尝试查看空队列的头部”);

   throw new list_err(“attempt to peek at head of empty queue”);

  返回列表::head()->val;

  return list::head()->val;

 }

 }

注意,当使用范围解析运算符( list: :)标识时, list类的 head 方法对于queue 类的方法仍然可见(但对其用户不可见!) 。■

Note that the head method of class list is still visible to methods of class queue (but not to its users!) when identified with the scope resolution operator (list::). ■

例 10.11

Example 10.11

访问基类成员

Accessing base class members

其他面向对象语言提供了访问基类成员的其他方法。在 Smalltalk、Objective-C、Java 和 C# 中,使用关键字basesuper

Other object-oriented languages provide other means of accessing the members of a base class. In Smalltalk, Objective-C, Java, and C#, one uses the keyword base or super:

列表::head();// C++
超级.head();// Java
基础.head();// C#
超级头。// 小型谈话
[超级头]// 目标 C

例 10.12

Example 10.12

在 Eiffel 中重命名方法

Renaming methods in Eiffel

在 Eiffel 中,必须明确重命名从基类继承的方法,才能使它们可访问:

In Eiffel, one must explicitly rename methods inherited from a base class, in order to make them accessible:

类队列

class queue

继承

inherit

 列表

 list

  重命名

  rename

   head 作为 list_head

   head as list_head

   …    ——其他重命名

   …   -- other renames

  结尾

  end

在队列的方法中,列表head方法可以作为list_head调用。C++ 和 Eiffel 不能使用关键字super,因为在存在多重继承的情况下,它会产生歧义。■

Within methods of queue, the head method of list can be invoked as list_head. C++ and Eiffel cannot use the keyword super, because it would be ambiguous in the presence of multiple inheritance. ■

对象作为其他对象的字段

Objects as Fields of Other Objects

例 10.13

Example 10.13

包含列表的队列

A queue that contains a list

作为从列表派生队列的替代方法,我们可以选择将列表作为队列字段

As an alternative to deriving queue from list, we might choose to include a list as a field of a queue instead:

类队列 {

class queue {

 列出内容;

 list contents;

民众:

public:

 bool 空() {

 bool empty() {

  返回内容.empty();

  return contents.empty();

 }

 }

 无效入队(const int v){

 void enqueue(const int v) {

  内容.附加(新列表节点(v));

  contents.append(new list_node(v));

 }

 }

 int 出队() {

 int dequeue() {

  如果 (空())

  if (empty())

   抛出新的 list_err(“尝试从空队列中出队”);

   throw new list_err(“attempt to dequeue from empty queue”);

  列表节点* p = 内容.head();

  list_node* p = contents.head();

  p->删除();

  p->remove();

  int v = p->val;

  int v = p->val;

  删除p;

  delete p;

  返回v;

  return v;

 }

 }

};

};

在这个例子中,实际差异很小。选择主要归结为我们是否将队列视为一种特殊的列表,或者我们是否将队列视为使用列表作为其实现的一部分的抽象。继承最引人注目的情况是我们希望能够在需要基类对象(例如“”)的上下文中使用派生类的对象(例如“客户端”),并让该对象由于属于派生类而以特殊方式行事(例如,在打印时包含额外信息)。我们将在第 10.4 节中考虑这些类型的情况。■

The practical difference is small in this example. The choice mainly boils down to whether we think of a queue as a special kind of list, or whether we think of a queue as an abstraction that uses a list as part of its implementation. The cases in which inheritance is most compelling are those in which we want to be able to use an object of a derived class (a “client,” say) in a context that expects an object of a base class (a “person,” say), and have that object behave in a special way by virtue of belonging to the derived class (e.g., include extra information when printed). We will consider these sorts of cases in Section 10.4. ■

10.1.1 类和泛型

10.1.1 Classes and Generics

精明的读者可能已经注意到,我们的各种列表和队列都嵌入了这样的假设:每个列表节点中的项都是整数。实际上,我们希望能够拥有多种项的列表和队列,所有这些都基于大量代码的单一副本。在 Ruby 或 Python 等动态类型语言中,这是很自然的:val 字段没有静态类型,任何类型的对象都可以添加到列表和队列中,也可以从中删除。

The astute reader may have noticed that our various lists and queues have all embedded the assumption that the item in each list node is an integer. In practice, we should like to be able to have lists and queues of many kinds of items, all based on a single copy of the bulk of the code. In a dynamically typed language like Ruby or Python, this is natural: the val field would have no static type, and objects of any kind could be added to, and removed from, lists and queues.

例 10.14

Example 10.14

通用列表的基类

Base class for general-purpose lists

在 C++ 等静态类型语言中,创建一个没有val字段的通用list_node类,然后派生添加值的子类(例如int_list_node)是很有诱惑力的。虽然这种方法可以奏效,但它有一些不幸的局限性。假设我们定义一个gp_list_node类型,其中包含实现列表操作所需的字段和方法,但没有val有效负载:

In a statically typed language like C++, it is tempting to create a general-purpose list_node class that has no val field, and then derive subclasses (e.g., int_list_node) that add the values. While this approach can be made to work, it has some unfortunate limitations. Suppose we define a gp_list_node type, with the fields and methods needed to implement list operations, but without a val payload:

类 gp_list_node {

class gp_list_node {

 gp_list_node * 上一个;

 gp_list_node* prev;

 gp_list_node*下一个;

 gp_list_node* next;

 gp_list_node* 头节点;

 gp_list_node* head_node;

民众:

public:

 gp_list_node();      // 假设方法主体单独给出。

 gp_list_node();     // assume method bodies given separately

 gp_list_node* 前任();

 gp_list_node* predecessor();

 gp_list_node* 后继();

 gp_list_node* successor();

 bool 单例();

 bool singleton();

 无效插入前(gp_list_node * new_node);

 void insert_before(gp_list_node* new_node);

 无效删除();

 void remove();

 ~gp_list_node();

 ~gp_list_node();

};

};

要创建可以在整数列表中使用的节点,我们需要一个val字段和一些构造函数:

To create nodes that can be used in a list of integers, we will need a val field and some constructors:

类 int_list_node:公共 gp_list_node {

class int_list_node : public gp_list_node {

民众:

public:

 int val;      //节点中的实际数据

 int val;     // the actual data in a node

 int_list_node() { val = 0; }

 int_list_node() { val = 0; }

 int_list_node(int v){ val = v; }

 int_list_node(int v) { val = v; }

 …

 …

初始化prevnexthead_node 字段将保留在gp_list_node构造函数中,每当我们创建int_list_node对象时都会自动调用该构造函数。singleton 、insert_beforeremove方法同样可以从gp_list_node完整继承,析构函数也是如此。■

Initialization of the prev, next, and head_node fields will remain in the hands of the gp_list_node constructor, which will be called automatically whenever we create a int_list_node object. The singleton, insert_before, and remove methods can likewise be inherited from gp_list_node intact, as can the destructor. ■

例 10.15

Example 10.15

类型特定扩展的问题

The problem with type-specific extensions

但是后继前任怎么办?如果我们保持它们不变,它们将继续返回gp_list_node类型的值,而不是int_list_node

But what about successor and predecessor? If we leave these unchanged, they will continue to return values of type gp_list_node, not int_list_node:

int_list_node* p = …// 任何
int v = p->successor().val// 无法编译!

就编译器而言,int_list_node的后继将没有val字段。为了解决这个问题,我们需要显式转换:

As far as the compiler is concerned, the successor of an int_list_node will have no val field. To fix the problem, we will need explicit casts:

int_list_node * 前任(){

int_list_node* predecessor() {

 返回 static_cast<int_list_node*>(gp_list_node::predecessor());

 return static_cast<int_list_node*>(gp_list_node::predecessor());

}

}

int_list_node* 后继节点() {

int_list_node* successor() {

 返回 static_cast<int_list_node*>(gp_list_node::successor());

 return static_cast<int_list_node*>(gp_list_node::successor());

}

}

类似地,我们可以创建一个通用列表类:

In a similar vein, we can create a general-purpose list class:

类 gp_list {

class gp_list {

 gp_list_node 头节点;

 gp_list_node head_node;

民众:

public:

 bool empty();      // 方法主体再次单独给出

 bool empty();     // method bodies again given separately

 gp_list_node* 头();

 gp_list_node* head();

 无效附加(gp_list_node *new_node);

 void append(gp_list_node *new_node);

 ~gp_list();

 ~gp_list();

};

};

但是如果我们扩展它来创建一个int_list类,我们将需要在head方法中进行强制转换:

But if we extend it to create an int_list class, we will need a cast in the head method:

类 int_list:公共 gp_list {

class int_list : public gp_list {

民众:

public:

 int_list_node* head() {      // 重新定义;隐藏原始

 int_list_node* head() {     // redefinition; hides original

  返回 static_cast<int_list_node*>(gp_list::head());

  return static_cast<int_list_node*>(gp_list::head());

 }

 }

};

};

假设我们编写的代码正确,我们的任何强制类型转换都不会引入错误。但是,如果我们的代码编写不正确,它们可能会阻止编译器捕获错误:

Assuming we write our code correctly, none of our casts will introduce bugs. They may, however, prevent the compiler from catching bugs if we write our code incorrectly:

类 string_list_node:公共 gp_list_node {

class string_list_node : public gp_list_node {

 // 类似于 int_list_node

 // analogous to int_list_node

 …

 …

};

};

string_list_node n(“嘘!”);

string_list_node n(“boo!”);

int_list L;

int_list L;

L.附加(&n);

L.append(&n);

cout << “0x” << hex << L.head()->val;

cout << “0x” << hex << L.head()->val;

关于作者64 位 Macbook 上,此代码打印“ 0x6f6f6208 ”。发生了什么?从gp_list继承的方法int_list::append需要一个类型为gp_list_node*的参数,并且由于string_list_node派生自gp_list_node ,因此可以接受指向节点n 的指针。但是当我们查看此节点时, L.head()中的强制类型转换告诉编译器不要抱怨,因为我们将该节点(无法证明它是比gp_list_node更具体的任何东西)视为我们确定它包含一个int 。并非巧合的是, 0x6f6f6208的高三个字节以相反的顺序包含字符“ boo ”的 ASCII 码。■

On the author's 64-bit Macbook, this code prints “0x6f6f6208.” What happened? Method int_list::append, inherited from gp_list, expects a parameter of type gp_list_node*, and since string_list_node is derived from gp_list_node, a pointer to node n is acceptable. But when we peek at this node, the cast in L.head() tells the compiler not to complain when we treat the node (which can't be proven to be anything more specific than a gp_list_node) as if we were certain it held an int. Not coincidentally, the upper three bytes of 0x6f6f6208 contain, in reverse order, the ASCII codes of the characters “boo.” ■

例 10.16

Example 10.16

如何命名未知类型?

How do you name an unknown type?

如果我们尝试定义示例 10.810.10中的队列的通用类似物,情况会变得更加糟糕:

Things get even worse if we try to define a general-purpose analogue of the queue from Examples 10.810.10:

类 gp_queue:私有 gp_list {

class gp_queue : private gp_list {

民众:

public:

 使用 gp_list::empty;

 using gp_list::empty;

 void enqueue(const ?? v);      //什么是“??”?

 void enqueue(const ?? v);     // what is “??” ?

 ?? 出队();

 ?? dequeue();

 ?? 头();

 ?? head();

};

};

当我们甚至不知道对象的类型时,我们如何谈论队列应该包含的对象?■

How do we talk about the objects the queue is supposed to contain when we don't even know their type? ■

例 10.17

Example 10.17

C++ 中的泛型列表

Generic lists in C++

答案当然是泛型(第 C-7.3.2 节)——C++ 中的模板。这些允许我们定义一个list_node<T>类,该类可以为任何数据类型T实例化,而无需继承或类型转换:

The answer, of course, is generics (Section C-7.3.2)—templates, in C++. These allow us to define a list_node<T> class that can be instantiated for any data type T, without the need for either inheritance or type casts:

模板<类型名称 V>

template<typename V>

类列表节点{

class list_node {

 列表节点<V> * 上一个;

 list_node<V>* prev;

 列表节点<V> * 下一个;

 list_node<V>* next;

 列表节点<V> * 头节点;

 list_node<V>* head_node;

民众:

public:

 V值;

 V val;

 list_node<V>* 前任() { …

 list_node<V>* predecessor() { …

 list_node<V>* successor() { …

 list_node<V>* successor() { …

 void insert_before(list_node<V> * new_node) { …

 void insert_before(list_node<V>* new_node) { …

 …

 …

};

};

模板<类型名称 V>

template<typename V>

班级列表 {

class list {

 list_node<V> 标题;

 list_node<V> header;

民众:

public:

 list_node<V>* head() { …

 list_node<V>* head() { …

 void append(list_node<V> *new_node) { …

 void append(list_node<V> *new_node) { …

 …

 …

};

};

模板<类型名称 V>

template<typename V>

类队列:私有列表<V> {

class queue : private list<V> {

民众:

public:

 使用列表 <V>::empty;

 using list<V>::empty;

 void enqueue(const V v){…

 void enqueue(const V v) { …

 V 出队() { …

 V dequeue() { …

 V 头() { …

 V head() { …

};

};

typedef list_node<int> int_list_node;

typedef list_node<int> int_list_node;

typedef list_node <string> string_list_node;

typedef list_node<string> string_list_node;

类型定义列表<int> int_list;

typedef list<int> int_list;

int_list_node n(3);

int_list_node n(3);

string_list_node s(“嘘!”);

string_list_node s(“boo!”);

int_list L;

int_list L;

L.append(&n);      // 成功

L.append(&n);     // ok

L.append(&s);      // 无法编译!

L.append(&s);     // will not compile!

设计与实现

Design & Implementation

10.2 容器/收藏品

10.2 Containers/collections

在面向对象编程中,保存某个给定类的对象集合的抽象通常称为容器常见容器包括有序和无序集合、堆栈、队列和字典,以列表、树、哈希表和各种其他具体数据结构实现。所有主要的面向对象语言都包含大量容器库。本节暗示了创建容器库所涉及的一些问题:哪些类派生自哪些类?什么时候我们说“X 是 Y”而不是“X 包含/使用 Y”?支持哪些操作,它们的时间复杂度是多少?每个操作会产生多少“内存流失”(堆分配和垃圾收集)?所有东西都是类型安全的吗?泛型的使用范围有多广?迭代容器内容有多容易?鉴于这些问题众多,设计安全、高效、灵活的容器库是一门复杂而困难的艺术。有关基于示例 10.14gp_list_node基类构建的方法,但仍利用模板来避免类型转换的需要,请参见练习 10.8

In object-oriented programming, an abstraction that holds a collection of objects of some given class is often called a container. Common containers include sorted and unsorted sets, stacks, queues, and dictionaries, implemented as lists, trees, hash tables, and various other concrete data structures. All of the major object-oriented languages include extensive container libraries. A few of the issues involved in their creation have been hinted at in this section: Which classes are derived from which others? When do we say that “X is a Y” instead of “X contains/uses a Y”? Which operations are supported, and what is their time complexity? How much “memory churn” (heap allocation and garbage collection) does each operation incur? Is everything type safe? How extensive is the use of generics? How easy is it to iterate over the contents of a container? Given these many questions, the design of safe, efficient, and flexible container libraries is a complex and difficult art. For an approach that builds on the gp_list_node base class of Example 10.14, but still leverages templates to avoid the need for type casts, see Exercise 10.8.

简而言之,泛型是为了抽象不相关的类型而存在的,这是继承所不支持的。除了 C++,泛型还出现在大多数其他静态类型的面向对象语言中,包括 Eiffel、Java、C# 和 OCaml。

In a nutshell, generics exist for the purpose of abstracting over unrelated types, something that inheritance does not support. In addition to C++, generics appear in most other statically typed object-oriented languages, including Eiffel, Java, C#, and OCaml.

10-01-9780124104099检查你的理解

Check Your Understanding

1. 一般认为面向对象编程的三个定义特征是什么?

1. What are generally considered to be the three defining characteristics of object-oriented programming?

2. 面向对象起源于 20 世纪 60 年代的哪种编程语言?谁发明了这种语言?总结自那时以来三个定义特征的演变。

2. In what programming language of the 1960s does object orientation find its roots? Who invented that language? Summarize the evolution of the three defining characteristics since that time.

3. 说出抽象的三个重要好处。

3. Name three important benefits of abstraction.

4. 子程序成员数据成员的常用名称有哪些?

4. What are the more common names for subroutine member and data member?

5.  C# 中的属性是什么?

5. What is a property in C#?

6. 对象接口的“私有”部分有什么用处?为什么它不能被完全隐藏?

6. What is the purpose of the “private” part of an object interface? Why can't it be hidden completely?

7.  C++ 中 :: 运算符的用途是什么?

7. What is the purpose of the :: operator in C++?

8. 解释为什么内联子程序在面向对象语言中尤为重要。

8. Explain why in-line subroutines are particularly important in object-oriented languages.

9. 什么是构造函数析构函数

9. What are constructors and destructors?

10.给出 基类派生类的另外两个术语。

10. Give two other terms, each, for base class and derived class.

11. 解释为什么尽管继承已经提供了广泛的多态性,泛型在面向对象语言中仍然很有用。

11. Explain why generics may be useful in an object-oriented language, despite the extensive polymorphism already provided by inheritance.

10.2 封装和继承

10.2 Encapsulation and Inheritance

封装机制使程序员能够将数据和对其进行操作的子程序组合到一个地方,并向抽象的用户隐藏不相关的细节。在上一节(以及第 3.3.5 节)中,我们将面向对象编程视为 Simula 和 Euclid 的“模块即类型”机制的扩展。也可以将面向对象编程视为 Simula 和 Euclid 的“模块即类型”机制的扩展。在“模块作为管理器”框架中进行面向对象编程。在下面的第一小节中,我们将考虑非面向对象语言中模块的数据隐藏机制。在第二小节中,我们将考虑在向模块添加继承时出现的新数据隐藏问题。在第三小节中,我们将简要回顾模块作为管理器方法,并展示包括 Ada 95 和 Fortran 2003 在内的几种语言如何向记录添加继承,从而允许(静态)模块继续提供数据隐藏。

Encapsulation mechanisms enable the programmer to group data and the subroutines that operate on them together in one place, and to hide irrelevant details from the users of an abstraction. In the preceding section (and likewise Section 3.3.5) we cast object-oriented programming as an extension of the “module-as-type” mechanisms of Simula and Euclid. It is also possible to cast object-oriented programming in a “module-as-manager” framework. In the first subsection below we consider the data-hiding mechanisms of modules in non-object-oriented languages. In the second subsection we consider the new data-hiding issues that arise when we add inheritance to modules. In the third subsection we briefly return to the module-as-manager approach, and show how several languages, including Ada 95 and Fortran 2003, add inheritance to records, allowing (static) modules to continue to provide data hiding.

10.2.1 模块

10.2.1 Modules

数据隐藏的范围规则是 Clu、Modula、Euclid 和 20 世纪 70 年代其他基于模块的语言的主要创新之一。在 Clu 和 Euclid 中,模块的声明和定义(头和主体)总是一起出现。在 Modula-2 中,程序员可以选择将头和主体放在单独的文件中。不幸的是,没有办法将头分为公共和私有部分;其中的所有内容都是公共的(即导出的)。数据隐藏的唯一让步是可以在头中声明指针类型,而无需透露它们指向的对象的结构。由于大多数机器上的指针大小相同,因此编译器可以为模块的用户生成代码(侧边栏 10.1),而无需隐藏信息。

Scope rules for data hiding were one of the principal innovations of Clu, Modula, Euclid, and other module-based languages of the 1970s. In Clu and Euclid, the declaration and definition (header and body) of a module always appeared together. In Modula-2, programmers had the option of placing the header and the body in separate files. Unfortunately, there was no way to divide the header into public and private parts; everything in it was public (i.e., exported). The only concession to data hiding was that pointer types could be declared in a header without revealing the structure of the objects to which they pointed. Compilers could generate code for the users of a module (Sidebar 10.1) without the hidden information, since pointers are all of equal size on most machines.

例 10.18

Example 10.18

Ada 中的数据隐藏

Data hiding in Ada

Ada 允许将包的头部分为公共和私有部分,从而提高了灵活性。通过将导出类型的细节放在头部的私有部分,并在公共部分中简单地命名类型,可以使导出类型的细节变得不透明:

Ada increases flexibility by allowing the header ofa package to be divided into public and private parts. Details of an exported type can be made opaque by putting them in the private part of the header and simply naming the type in the public part:

软件包 foo 是     -- 标头

package foo is     -- header

 …

 …

 类型 T 是私有的;

 type T is private;

 …

 …

private      – 以下定义用户无法访问

private     -- definitions below here are inaccessible to users

 …

 …

 类型 T 是...——     完整定义

 type T is …     -- full definition

 …

 …

结束 foo;

end foo;

私有部分提供编译器“在线”分配对象所需的信息。对模块主体的更改绝不会强制重新编译模块的任何用户。对模块头的私有部分的更改可能会强制重新编译,但它绝不会要求更改用户的源代码。对头的公共部分的更改是对模块接口的更改:它通常需要我们更改用户的代码。■

The private part provides the information the compiler needs to allocate objects “in line.” A change to the body of a module never forces recompilation of any of the users of the module. A change to the private part of the module header may force recompilation, but it never requires changes to the source code of the users. A change to the public part of a header is a change to the module's interface: it will often require us to change the code of users. ■

由于静态管理器式模块仅影响名称的可见性,因此不会引入特殊的代码生成问题。模块内部变量和其他数据的存储管理方式与模块外部数据的存储管理方式完全相同。如果模块出现在全局范围内,则其数据可以静态分配。如果模块出现在子程序中,那么当调用子程序时,其数据可以在堆栈上以已知偏移量分配,并在返回时回收。

Because they affect only the visibility of names, static, manager-style modules introduce no special code generation issues. Storage for variables and other data inside a module is managed in precisely the same way as storage for data immediately outside the module. If the module appears in a global scope, then its data can be allocated statically. If the module appears within a subroutine, then its data can be allocated on the stack, at known offsets, when the subroutine is called, and reclaimed when it returns.

模块类型,如 Euclid 和 ML 中的模块类型,稍微复杂一些:它们允许模块拥有任意数量的实例。显而易见的实现类似于记录的实现。如果模块中的所有数据都具有静态已知的大小,则可以为每个单独的数据分配模块存储中的静态偏移量。如果某些数据的大小直到运行时才知道,则可以将模块的存储分为固定大小和可变大小的部分,在固定大小部分的开头有一个内幕向量(描述符)。可以根据需要在堆栈或堆中静态分配模块的实例。

Module types, as in Euclid and ML, are somewhat more complicated: they allow a module to have an arbitrary number of instances. The obvious implementation then resembles that of a record. If all of the data in the module have a statically known size, then each individual datum can be assigned a static offset within the module's storage. If the size of some of the data is not known until run time, then the module's storage can be divided into fixed-size and variable-size portions, with a dope vector (descriptor) at the beginning of the fixed-size portion. Instances of the module can be allocated statically, on the stack, or in the heap, as appropriate.

this参数

The “this“ Parameter

例 10.19

Example 10.19

隐藏this参数

The hidden this parameter

模块内的子程序还会出现另一个复杂情况。它们如何知道要使用哪些变量?当然,我们可以在模块的每个实例中复制每个子程序的代码,就像复制数据一样。但是,这种复制会非常浪费,因为副本仅在地址计算的细节上有所不同。更好的方法是为每个模块子程序创建一个实例,并在运行时将相应模块实例的存储地址传递给该实例。此地址采用每个模块子程序的额外隐藏第一个参数的形式。Euclid 调用的形式为

One additional complication arises for subroutines inside a module. How do they know which variables to use? We could, of course, replicate the code for each subroutine in each instance of the module, just as we replicate the data. This replication would be highly wasteful, however, as the copies would vary only in the details of address computations. A better technique is to create a single instance of each module subroutine, and to pass that instance, at run time, the address of the storage of the appropriate module instance. This address takes the form of an extra, hidden first parameter for every module subroutine. A Euclid call of the form

my_stack.push(x)

my_stack.push(x)

被翻译成好像真的

is translated as if it were really

推送(my_stack,x)

push(my_stack, x)

其中,my_stack通过引用传递。在面向对象语言中,也会发生同样的转换。■

where my_stack is passed by reference. The same translation occurs in object-oriented languages. ■

不使用模块头文件

Making Do without Module Headers

如第 C-3.8 节所述,Java 包和 C/C++/C# 命名空间可以分布在多个编译单元(文件)中。在 C、C++ 和 C# 中,单个文件也可以包含多个命名空间的片段。更重要的是,许多现代语言(包括 Java 和 C#)都摒弃了单独的标头和主体的概念。虽然程序员仍然必须定义接口(并通过公共声明指定它),但无需手动识别出于实现原因需要在标头中的代码:相反,编译器负责从模块的全文中自动提取此信息。出于软件工程目的,可能仍然需要创建模块的初步“骨架”版本,以便可以针对该版本编译其他模块,但这是可选的。为了协助项目管理和文档编制,许多 Java 和 C# 实现都提供了一种工具,可以从模块的完整文本中提取用户所需的最少信息。

As noted in Section C-3.8, Java packages and C/C++/C# namespaces can be spread across multiple compilation units (files). In C, C++, and C#, a single file can also contain pieces of more than one namespace. More significantly, many modern languages, including Java and C#, dispense with the notion of separate headers and bodies. While the programmer must still define the interface (and specify it via public declarations), there is no need to manually identify code that needs to be in the header for implementation reasons: instead the compiler is responsible for extracting this information automatically from the full text of the module. For software engineering purposes it may still be desirable to create preliminary “skeleton” versions of a module, against which other modules can be compiled, but this is optional. To assist in project management and documentation, many Java and C# implementations provide a tool that will extract from the complete text of a module the minimum information required by its users.

10.2.2 类

10.2.2 Classes

随着继承的引入,面向对象语言必须补充基于模块的语言的范围规则以解决其他问题。例如,基类应该对其成员在派生类中的可见性进行多大程度的控制?基类的私有成员是否应该对派生类的方法可见?基类的公共成员是否应该始终是派生类的公共成员(即对派生类的用户可见)?

With the introduction of inheritance, object-oriented languages must supplement the scope rules of module-based languages to cover additional issues. For example, how much control should a base class exercise over the visibility of its members in derived classes? Should private members of a base class be visible to methods of a derived class? Should public members of a base class always be public members of a derived class (i.e., be visible to users of the derived class)?

例 10.20

Example 10.20

隐藏继承的方法

Hiding inherited methods

我们在示例 10.9中提到了这些问题,其中我们将类队列声明为私有列表,对派生类的用户隐藏基类的公共成员 — 除了方法empty ,我们使用 using声明再次明确显示该方法。C++ 也允许相反的策略:可以从派生类中明确删除原本是公共的基类的方法:

We touched on these questions in Example 10.9, where we declared class queue as a private list, hiding public members of the base class from users of the derived class—except for method empty, which we made explicitly visible again with a using declaration. C++ allows the inverse strategy as well: methods of an otherwise public base class can be explicitly deleted from the derived class:

类队列:公共列表{

class queue : public list {

 

 

 void append(list_node *new_node) = 删除;

 void append(list_node *new_node) = delete;

在Eiffel、Python和Ruby中也可以找到类似的删除机制。■

Similar deletion mechanisms can be found in Eiffel, Python, and Ruby. ■

除了publicprivate标签之外,C++ 还允许将类的成员指定为protected。 protected 成员仅对其自身类或从该类派生的类的方法可见。在我们的示例中,list的protected 成员M不仅可由list本身的方法访问,还可由queue的方法访问。但是,与 public 成员不同,M对listqueue对象的任意用户都是不可见的。

In addition to the public and private labels, C++ allows members of a class to be designated protected. A protected member is visible only to methods of its own class or of classes derived from that class. In our examples, a protected member M of list would be accessible not only to methods of list itself but also to methods of queue. Unlike public members, however, M would not be visible to arbitrary users of list or queue objects.

例 10.21

Example 10.21

C++ 中的受保护基类

protected base class in C++

指定基类时也可以使用protected关键字:

The protected keyword can also be used when specifying a base class:

派生类:受保护的基类 { …

class derived : protected base { …

这里基类的公共成员表现得像派生类的受保护成员。■

Here public members of the base class act like protected members of the derived class. ■

C++可见性规则背后的基本哲学可以概括如下:

The basic philosophy behind the visibility rules of C++ can be summarized as follows:

 任何类都可以限制其成员的可见性。公共成员在类声明范围内的任何地方都可见。私有成员仅在类的方法内可见。受保护的成员在类或其后代的方法内可见。(作为正常规则的例外,类可以指定某些其他朋友类或子例程应有权访问其私有成员。)

 Any class can limit the visibility of its members. Public members are visible anywhere the class declaration is in scope. Private members are visible only inside the class's methods. Protected members are visible inside methods of the class or its descendants. (As an exception to the normal rules, a class can specify that certain other friend classes or subroutines should have access to its private members.)

 派生类可以限制基类成员的可见性,但永远不能增加其可见性。3类的私有成员在派生类中永远不可见。类。公共基类的受保护成员和公共成员在派生类中分别是受保护成员或公共成员。受保护基类的受保护成员和公共成员是派生类的受保护成员。私有基类的受保护成员和公共成员是派生类的私有成员。

 A derived class can restrict the visibility of members of a base class, but can never increase it.3 Private members of a base class are never visible in a derived class. Protected and public members of a public base class are protected or public, respectively, in a derived class. Protected and public members of a protected base class are protected members of a derived class. Protected and public members of a private base class are private members of a derived class.

■通过将基类声明 为 protectedprivate来限制基类成员可见性的派生类可以通过在派生类声明的protectedpublic部分插入using声明来恢复基类各个成员的可见性。

 A derived class that limits the visibility of members of a base class by declaring that base class protected or private can restore the visibility of individual members of the base class by inserting a using declaration in the protected or public portion of the derived class declaration.

■派生类可以通过显式地 delete来使基类的方法(但不是字段)不可访问(对其他方法和其自身而言)。

 A derived class can make methods (though not fields) of a base class inaccessible (to others and to itself) by explicitly delete-ing them.

其他面向对象语言对可见性采用不同的方法。Eiffel 在它可以支持的可见性模式方面比 C++ 更灵活,但它不遵守上述 C++ 原则中的第一条。Eiffel 中的派生类可以同时限制和增加基类成员的可见性。每个方法(在 Eiffel 中称为特性)都可以指定自己的导出状态。如果状态为{NONE},则成员实际上是私有的(在 Eiffel 中称为机密)。如果状态为{ANY},则成员实际上是公共的(在 Eiffel 中称为通常可用)。在一般情况下,状态可以是任意的类名列表,在这种情况下,该特性被认为仅对这些类及其后代有选择地可用。任何从基类继承的特性都可以在派生类中被赋予新状态。

Other object-oriented languages take different approaches to visibility. Eiffel is more flexible than C++ in the patterns of visibility it can support, but it does not adhere to the first of the C++ principles above. Derived classes in Eiffel can both restrict and increase the visibility of members of base classes. Every method (called a feature in Eiffel) can specify its own export status. If the status is {NONE} then the member is effectively private (called secret in Eiffel). If the status is {ANY} then the member is effectively public (called generally available in Eiffel). In the general case the status can be an arbitrary list of class names, in which case the feature is said to be selectively available to those classes and their descendants only. Any feature inherited from a base class can be given a new status in a derived class.

Java 和 C# 在声明publicprotectedprivate成员时遵循 C++,但不提供对基类的protectedprivate指定;派生类既不能增加也不能限制基类成员的可见性。但是,它可以通过定义具有相同名称的新方法隐藏字段或覆盖方法;由于缺少范围解析运算符,新类的用户无法访问旧成员。在 Java 中,方法的覆盖版本不能比基类中的版本具有更严格的可见性限制。

Java and C# follow C++ in the declaration of public, protected, and private members, but do not provide the protected and private designations for base classes; a derived class can neither increase nor restrict the visibility of members of a base class. It can, however, hide a field or override a method by defining a new one with the same name; the lack of a scope resolution operator makes the old member inaccessible to users of the new class. In Java, the overriding version of a method cannot have more restrictive visibility than the version in the base class.

protected关键字在 Java 中的含义与在 C++ 中略有不同: Java 类的受保护成员不仅在派生类中可见,而且在声明该类的整个包(命名空间)中也可见。Java 中没有显式访问修饰符的类成员在声明该类的整个包中可见,但在其他包中的任何派生类中不可见。C# 定义protected与 C++ 一样,但提供了一个额外的内部关键字,使成员在类所在的整个程序集中可见。(程序集是链接在一起的编译单元的集合,相当于Java 中的.jar文件。)默认情况下,C# 类的成员是私有的

The protected keyword has a slightly different meaning in Java than it does in C++: a protected member of a Java class is visible not only within derived classes but also within the entire package (namespace) in which the class is declared. A class member with no explicit access modifier in Java is visible throughout the package in which the class is declared, but not in any derived classes that reside in other packages. C# defines protected as C++ does, but provides an additional internal keyword that makes a member visible throughout the assembly in which the class appears. (An assembly is a collection of linked-together compilation units, comparable to a .jar file in Java.) Members of a C# class are private by default.

在 Smalltalk 和 Objective-C 中,成员可见性问题从未出现:语言允许代码在运行时尝试调用任何对象中的任何方法名称。如果方法存在(具有正确数量的参数),则调用继续;否则会导致运行时错误。在这些语言中,没有办法让某个方法对程序的某些部分可用,而对其他部分不可用。与此相关,Python 类成员始终是公共的。在 Ruby 中,字段始终是私有的;不仅如此,它们只能由其所属的单个对象的方法访问。

In Smalltalk and Objective-C, the issue of member visibility never arises: the language allows code at run time to attempt a call of any method name in any object. If the method exists (with the right number of parameters), then the invocation proceeds; otherwise a run-time error results. There is no way in these languages to make a method available to some parts of a program but not to others. In a related vein, Python class members are always public. In Ruby, fields are always private; more than that, they are accessible only to methods of the individual object to which they belong.

静态字段和方法

Static Fields and Methods

与publicprivateprotected所暗示的可见性正交,大多数面向对象语言允许将各个字段和方法声明为static。静态类成员被认为“属于”整个类,而不是任何单个对象。因此,它们有时被称为字段和方法,而不是实例字段和方法。(此术语在创建特殊元对象来表示每个类的语言中最常见 - 参见示例 10.26。类字段和方法被认为属于元对象。)每个静态字段的单一副本由其类的所有实例共享:一个对象的方法中对该字段所做的更改将对该类的所有其他对象的方法可见。静态方法没有 this 参数显式或隐式);它不能访问非静态(实例)字段。另一方面,非静态(实例)方法可以访问静态和非静态字段。

Orthogonal to the visibility implied by public, private, or protected, most object-oriented languages allow individual fields and methods to be declared static. Static class members are thought of as “belonging” to the class as a whole, not to any individual object. They are therefore sometimes referred to as class fields and methods, as opposed to instance fields and methods. (This terminology is most common in languages that create a special metaobject to represent each class—see Example 10.26. The class fields and methods are thought of as belonging to the metaobject.) A single copy of each static field is shared by all instances of its class: changes made to that field in methods of one object will be visible to methods of all other objects of the class. A static method, for its part, has no this parameter (explicit or implicit); it cannot access nonstatic (instance) fields. A nonstatic (instance) method, on the other hand, can access both static and nonstatic fields.

10.2.3 嵌套(内部类)

10.2.3 Nesting (Inner Classes)

例 10.22

Example 10.22

Java 中的内部类

Inner classes in Java

许多语言允许类声明嵌套。这立即引发了一个问题:如果Inner是Outer的成员,那么Inner的方法是否可以看到Outer的成员?如果可以,它们可以看到哪个实例?最简单的答案(在 C++ 和 C# 中采用)是仅允许访问外部类的静态成员,因为这些成员只有一个实例。实际上,嵌套只是一种信息隐藏手段。Java 采用了更复杂的方法。它允许嵌套(内部)类访问其周围类的任意成员。因此,内部类的每个实例都必须属于外部类的一个实例。

Many languages allow class declarations to nest. This raises an immediate question: if Inner is a member of Outer, can Inner's methods see Outer's members, and if so, which instance do they see? The simplest answer, adopted in C++ and C#, is to allow access to only the static members of the outer class, since these have only a single instance. In effect, nesting serves simply as a means of information hiding. Java takes a more sophisticated approach. It allows a nested (inner) class to access arbitrary members of its surrounding class. Each instance of the inner class must therefore belong to an instance of the outer class.

外部类 {

class Outer {

 int n;

 int n;

 类内部{

 class Inner {

  公共无效栏(){n = 1;}

  public void bar() { n = 1; }

 }

 }

 內部 i;

 Inner i;

 Outer() { i = new Inner(); }      // 构造函数

 Outer() { i = new Inner(); }     // constructor

 公共无效foo(){

 public void foo() {

  n=0;

  n = 0;

  System.out.println(n);      // 打印 0

  System.out.println(n);     // prints 0

  i.bar();

  i.bar();

  System.out.println(n);      // 打印 1

  System.out.println(n);     // prints 1

 }

 }

}

}

如果有多个Outer实例,则每个实例将具有不同的n,并且对Inner.bar的调用将访问相应的n。要实现这一点,每个Inner实例(当然可能有任意数量)必须包含指向其所属的Outer实例的隐藏指针。如果 Java 中的嵌套类被声明为static,则其行为与 C++ 和 C# 中的行为相同,只能访问周围类的静态成员。

If there are multiple instances of Outer, each instance will have a different n, and calls to Inner.bar will access the appropriate n. To make this work, each instance of Inner (of which there may of course be an arbitrary number) must contain a hidden pointer to the instance of Outer to which it belongs. If a nested class in Java is declared to be static, it behaves as in C++ and C#, with access to only the static members of the surrounding class.

Java 类也可以嵌套在方法中。这样的本地类不仅可以访问周围类的所有成员,还可以访问其嵌套方法的参数和变量。问题在于,嵌套类实际使用的任何参数或变量都必须是“有效最终的”——要么明确声明为最终的,要么至少在嵌套类详述后从未被修改(由嵌套类、周围方法或任何其他代码修改)。此规则允许实现复制引用的对象,而不是维护对周围方法框架的引用(即静态链接)。■

Java classes can also be nested inside methods. Such a local class has access not only to all members of the surrounding class but also to the parameters and variables of the method in which it is nested. The catch is that any parameters or variables that the nested class actually uses must be “effectively final”—either declared final explicitly or at least never modified (by the nested class, the surrounding method, or any other code) after the nested class is elaborated. This rule permits the implementation to make a copy of the referenced objects rather than maintaining a reference (i.e., a static link) to the frame of the surrounding method. ■

Java 中的内部类和本地类被广泛用于创建对象闭包,如第 3.6.3 节所述。在第 9.6.2 节中,我们将它们用作事件处理程序。我们还注意到,Java 中的本地类可以是匿名的:它可以以内联方式出现在对new 的调用中(示例 9.54)。

Inner and local classes in Java are widely used to create object closures, as described in Section 3.6.3. In Section 9.6.2 we used them as handlers for events. We also noted that a local class in Java can be anonymous: it can appear, in-line, inside a call to new (Example 9.54).

10.2.4 类型扩展

10.2.4 Type Extensions

Smalltalk、Objective-C、Eiffel、C++、Java 和 C# 都是从一开始就设计为面向对象的语言,要么从头开始,要么从没有强大封装机制的现有语言开始。它们都支持模块作为类型的抽象方法,其中单一机制(类)同时提供封装和继承。其他几种语言,包括 Modula-3 和 Oberon(均为 Modula-2 的后继者)、CLOS、Ada 95/2005 和 Fortran 2003,可以被描述为对模块已经提供封装的语言的面向对象扩展。这些语言不会改变现有的模块机制,而是通过扩展记录的机制提供继承和动态方法绑定。

Smalltalk, Objective-C, Eiffel, C++, Java, and C# were all designed from the outset as object-oriented languages, either starting from scratch or from an existing language without a strong encapsulation mechanism. They all support a module-as-type approach to abstraction, in which a single mechanism (the class) provides both encapsulation and inheritance. Several other languages, including Modula-3 and Oberon (both successors to Modula-2), CLOS, Ada 95/2005, and Fortran 2003, can be characterized as object-oriented extensions to languages in which modules already provide encapsulation. Rather than alter the existing module mechanism, these languages provide inheritance and dynamic method binding through a mechanism for extending records.

例 10.23

Example 10.23

Ada 2005 中的列表和队列抽象

List and queue abstractions in Ada 2005

在 Ada 2005 中,列表和队列抽象可以如图10.2所示定义。为了控制对类型结构的访问,我们将它们隐藏在 Ada 包中。g_list.queue的过程initializefinalizeenqueuedequeue可以将其参数self转换为list_ptr ,因为队列是列表的扩展。包g_list.queue被称为包g_list的子包,因为它的名称以其父包的名称为前缀。Ada 中的子包类似于 Eiffel 或 C++ 中的派生类,只不过它仍然是一个管理器,而不是类型。与 Eiffel 类似,但与 C++ 不同,Ada 允许子包的主体查看父包的私有部分。

In Ada 2005, our list and queue abstractions could be defined as shown in Figure 10.2. To control access to the structure of types, we hide them inside Ada packages. The procedures initialize, finalize, enqueue, and dequeue of g_list.queue can convert their parameter self to a list_ptr, because queue is an extension of list. Package g_list.queue is said to be a child of package g_list because its name is prefixed with that of its parent. A child package in Ada is similar to a derived class in Eiffel or C++, except that it is still a manager, not a type. Like Eiffel, but unlike C++, Ada allows the body of a child package to see the private parts of the parent package.

f10-02-9780124104099f10-03-9780124104099
图 10.2 Ada 2005 中的通用列表和队列抽象。标记类型列表队列提供继承;包提供封装。声明self具有类型访问 XX(而不是XX_ptr)会导致编译器将子例程识别为标记类型的方法;如果ptr引用标记类型的对象,则ptr.method ( args ) 是method ( ptr , args )的语法糖。函数delete_node(下一页)使用Unchecked_Deallocation库包创建特定于类型的内存回收例程。表达式list_ptr(self)是(类型安全的)强制转换。

图 10.2中的所有列表和队列子例程都采用显式第一个参数。Ada 95 和 CLOS 不使用“ object.method() ”表示法。Python 和 Ada 2005 确实使用了这种表示法,但只是作为语法糖:调用AB(C, D) 被解释为对B(A, C, D)的调用,其中B被声明为一个三参数子例程。任意 Ada 代码都可以将类型为队列的对象传递给任何需要列表的例程;与 Java 一样,派生类型无法隐藏基类型的公共成员。■

All of the list and queue subroutines in Figure 10.2 take an explicit first parameter. Ada 95 and CLOS do not use “object.method()“ notation. Python and Ada 2005 do use this notation, but only as syntactic sugar: a call to A.B(C, D) is interpreted as a call to B(A, C, D), where B is declared as a three-parameter subroutine. Arbitrary Ada code can pass an object of type queue to any routine that expects a list; as in Java, there is no way for a derived type to hide the public members of a base type. ■

10.2.5 无需继承的扩展

10.2.5 Extending without Inheritance

扩展现有抽象的功能是面向对象编程的主要动机之一。继承是使这种扩展成为可能的标准机制。但是,有时继承不是一种选择,特别是在处理现有代码时。想要扩展的类可能不允许继承,例如:在 Java 中,它可能被标记为final;在 C# 中,它可能被标记为sealed。即使原则上可以继承,也可能有大量现有代码使用原始类名,并且返回并更改所有变量和参数声明以使用新的派生类型可能不可行。

The desire to extend the functionality ofan existing abstraction is one ofthe principal motivations for object-oriented programming. Inheritance is the standard mechanism that makes such extension possible. There are times, however, when inheritance is not an option, particularly when dealing with preexisting code. The class one wants to extend may not permit inheritance, for instance: in Java, it may be labeled final; in C#, it may be sealed. Even if inheritance is possible in principle, there may be a large body of existing code that uses the original class name, and it may not be feasible to go back and change all the variable and parameter declarations to use a new derived type.

例 10.24

Example 10.24

C# 中的扩展方法

Extension methods in C#

对于此类情况,C# 提供了扩展方法,可以将其视为对现有类的扩展:

For situations like these, C# provides extension methods, which give the appearance of extending an existing class:

静态类 AddToString {

static class AddToString {

 公共静态int toInt(此字符串s){

 public static int toInt(this string s) {

  返回 int.Parse(s);

  return int.Parse(s);

 }

 }

}

}

扩展方法必须是static 的,并且必须在静态类中声明。其第一个参数必须以关键字this作为前缀。然后可以调用该方法,就好像它是this所属类的成员一样:

An extension method must be static, and must be declared in a static class. Its first parameter must be prefixed with the keyword this. The method can then be invoked as if it were a member of the class of which this is an instance:

int n = myString.toInt();

int n = myString.toInt();

总之,方法声明和使用是

Together, the method declaration and use are syntactic sugar for

静态类 AddToString {

static class AddToString {

 public static int toInt (string s) {      // 没有 'this'

 public static int toInt (string s) {     // no 'this'

  返回 int.Parse(s);

  return int.Parse(s);

 }

 }

}

}

int n = AddToString.toInt(myString);

int n = AddToString.toInt(myString);

扩展方法没有特殊功能。具体来说,它们无法访问其扩展类的私有成员,也不支持动态方法绑定(第 10.4 节)。相比之下,包括 JavaScript 和 Ruby 在内的几种脚本语言确实允许程序员向现有类甚至单个对象添加新方法。我们将在第 14.4.4 节中进一步探讨这些选项。

No special functionality is available to extension methods. In particular, they cannot access private members of the class that they extend, nor do they support dynamic method binding (Section 10.4). By contrast, several scripting languages, including JavaScript and Ruby, really do allow the programmer to add new methods to existing classes—or even to individual objects. We will explore these options further in Section 14.4.4.

10-01-9780124104099检查你的理解

Check Your Understanding

12. 模块的不透明导出是什么意思?

12. What is meant by an opaque export from a module?

13.  Ada 中的私有类型是什么?

13. What are private types in Ada?

14.解释 面向对象语言中this参数的意义。

14. Explain the significance of the this parameter in object-oriented languages.

15.  Java 和 C# 如何在没有显式类头的情况下实现?

15. How do Java and C# make do without explicit class headers?

16.解释 C++ 中私有受保护公共类成员之间的区别。

16. Explain the distinctions among private, protected, and public class members in C++.

17.解释 C++ 中 私有受保护公共基类之间的区别。

17. Explain the distinctions among private, protected, and public base classes in C++.

18.描述 Eiffel 中的选择性可用性概念。

18. Describe the notion of selective availability in Eiffel.

19.  Smalltalk 和 Objective-C 中成员名称可见性规则与大多数其他面向对象语言的规则有何不同?

19. How do the rules for member name visibility in Smalltalk and Objective-C differ from the rules of most other object-oriented languages?

20. Java 中的 内部类与大多数其他嵌套类有何不同

20. How do inner classes in Java differ from most other nested classes?

21. 描述 Smalltalk、Eiffel 和 C++ 与 Ada、CLOS 和 Fortran 的面向对象特性之间的关键设计差异。

21. Describe the key design difference between the object-oriented features of Smalltalk, Eiffel, and C++ on the one hand, and Ada, CLOS, and Fortran on the other.

22. C# 中的 扩展方法是什么?它们有什么用途?

22. What are extension methods in C#? What purpose do they serve?

10.3 初始化和终止

10.3 Initialization and Finalization

第 3.2 节中,我们将对象的生命周期定义为对象占用空间并因此可以保存数据的时间间隔。大多数面向对象语言都提供了某种特殊机制,可以在对象生命周期开始时自动初始化对象。当以子程序形式编写时,此机制称为构造函数。虽然名称可能被认为暗示了其他含义,但构造函数不会分配空间;它会初始化已经分配的空间。一些语言提供了类似的析构函数机制,可以在对象生命周期结束时自动终止对象。出现了几个重要问题:

In Section 3.2 we defined the lifetime of an object to be the interval during which it occupies space and can thus hold data. Most object-oriented languages provide some sort of special mechanism to initialize an object automatically at the beginning of its lifetime. When written in the form of a subroutine, this mechanism is known as a constructor. Though the name might be thought to imply otherwise, a constructor does not allocate space; it initializes space that has already been allocated. A few languages provide a similar destructor mechanism to finalize an object automatically at the end of its lifetime. Several important issues arise:

选择构造函数:面向对象语言可能允许一个类具有零个、一个或多个不同的构造函数。在后一种情况下,不同的构造函数可能具有不同的名称,或者可能需要通过参数的数量和类型来区分它们。

Choosing a constructor: An object-oriented language may permit a class to have zero, one, or many distinct constructors. In the latter case, different constructors may have different names, or it may be necessary to distinguish among them by number and types of arguments.

引用和值:如果变量是引用,那么每个对象都必须显式创建,并且很容易确保调用适当的构造函数。如果变量是值,那么对象的创建可以作为细化的结果而隐式发生。在后一种情况下,语言必须允许对象在未初始化的情况下开始其生命周期,或者必须为每个精心设计的对象提供一种选择合适构造函数的方法。

References and values: If variables are references, then every object must be created explicitly, and it is easy to ensure that an appropriate constructor is called. If variables are values, then object creation can happen implicitly as a result of elaboration. In this latter case, the language must either permit objects to begin their lifetime uninitialized, or it must provide a way to choose an appropriate constructor for every elaborated object.

执行顺序:在 C++ 中创建派生类的对象时,编译器保证所有基类的构造函数都将先执行最外层的,然后再执行派生类的构造函数。此外,如果某个类的成员本身是某个类的对象,那么这些成员的构造函数将在包含它们的对象的构造函数之前被调用。这些规则是语法和语义复杂性的来源:当与多个构造函数、详尽的对象和多重继承相结合时,它们有时会在控制进入给定范围之前引发复杂的嵌套构造函数调用序列,并进行过载解析。其他语言的规则更简单。

Execution order: When an object of a derived class is created in C++, the compiler guarantees that the constructors for any base classes will be executed, outermost first, before the constructor for the derived class. Moreover, if a class has members that are themselves objects of some class, then the constructors for the members will be called before the constructor for the object in which they are contained. These rules are a source of considerable syntactic and semantic complexity: when combined with multiple constructors, elaborated objects, and multiple inheritance, they can sometimes induce a complicated sequence of nested constructor invocations, with overload resolution, before control even enters a given scope. Other languages have simpler rules.

垃圾收集:大多数面向对象语言都提供某种构造函数机制。析构函数相对较少见。它们的主要目的是方便 C++ 等语言中的手动存储回收。如果语言实现自动收集垃圾,那么对析构函数的需求就会大大减少。

Garbage collection: Most object-oriented languages provide some sort of constructor mechanism. Destructors are comparatively rare. Their principal purpose is to facilitate manual storage reclamation in languages like C++. If the language implementation collects garbage automatically, then the need for destructors is greatly reduced.

在本节的其余部分,我们将更详细地讨论这些问题。

In the remainder of this section we consider these issues in more detail.

10.3.1 选择构造函数

10.3.1 Choosing a Constructor

例 10.25

Example 10.25

在 Eiffel 中命名构造函数

Naming constructors in Eiffel

Smalltalk、Eiffel、C++、Java 和 C# 都允许程序员为给定类指定多个构造函数。在 C++、Java 和 C# 中,构造函数的行为类似于重载子例程:必须通过参数的数量和类型来区分它们。在 Smalltalk 和 Eiffel 中,不同的构造函数可以有不同的名称;创建对象的代码必须明确命名构造函数。在 Eiffel 中,有人可能会说

Smalltalk, Eiffel, C++, Java, and C# all allow the programmer to specify more than one constructor for a given class. In C++, Java, and C#, the constructors behave like overloaded subroutines: they must be distinguished by their numbers and types of arguments. In Smalltalk and Eiffel, different constructors can have different names; code that creates an object must name a constructor explicitly. In Eiffel one might say

复杂类

class COMPLEX

创建

creation

 新笛卡尔、新极坐标

 new_cartesian, new_polar

特征 {ANY}

feature {ANY}

 x, y :实数

 x, y : REAL

 new_cartesian(x_val,y_val:REAL)是

 new_cartesian(x_val, y_val : REAL) is

 

 do

  x := x_val;y := y_val

  x := x_val; y := y_val

 结尾

 end

 new_polar(rho, theta : REAL) 是

 new_polar(rho, theta : REAL) is

 

 do

  x := rho * cos(theta)

  x := rho * cos(theta)

  y := rho * sin(theta)

  y := rho * sin(theta)

 结尾

 end

 -- 其他公共方法

 -- other public methods

功能 {无}

feature {NONE}

 -- 私有方法

 -- private methods

结束——复杂类

end -- class COMPLEX

a, b :复杂

a, b : COMPLEX

!!b.新_笛卡尔(0,1)

!!b.new_cartesian(0, 1)

!!a.新极坐标(1,pi/2)

!!a.new_polar(1, pi/2)

!!操作符相当于 Eiffel 中的new。因为COMPLEX类 指定的构造函数(“创建者”)方法,编译器将坚持要求每次使用!!时都指定构造函数名称和参数。C++ 中没有此代码的直接模拟;两个构造函数都采用两个参数这一事实意味着它们无法通过重载来区分。■

The !! operator is Eiffel's equivalent of new. Because class COMPLEX specified constructor (“creator”) methods, the compiler will insist that every use of !! specify a constructor name and arguments. There is no straightforward analog of this code in C++; the fact that both constructors take two real arguments means that they could not be distinguished by overloading. ■

Smalltalk 在使用多个命名构造函数方面与 Eiffel 相似,但它更明确地区分属于单个对象的操作和属于一类对象的操作。Smalltalk 还采用了一种拟人化编程模型,其中每个操作都看作是由某个特定对象响应来自其他对象的请求(“消息”)而执行的。由于对象 O创建自身没有多大意义,因此O必须由代表O所属类的其他对象(称之为C)创建。当然,因为C是一个对象,所以它本身必须属于某个类。这种推理的结果是一个系统,其中每个类定义实际上都会引入一类和一对对象来表示它们。Objective-C 和 CLOS 具有类似的双重层次结构,Python 和 Ruby 也是如此。

Smalltalk resembles Eiffel in the use of multiple named constructors, but it distinguishes more sharply between operations that pertain to an individual object and operations that pertain to a class of objects. Smalltalk also adopts an anthropomorphic programming model in which every operation is seen as being executed by some specific object in response to a request (a “message”) from some other object. Since it makes little sense for an object O to create itself, O must be created by some other object (call it C) that represents O's class. Of course, because C is an object, it must itself belong to some class. The result of this reasoning is a system in which each class definition really introduces a pair of classes and a pair of objects to represent them. Objective-C and CLOS have similar dual hierarchies, as do Python and Ruby.

例 10.26

Example 10.26

Smalltalk 中的元类

Metaclasses in Smalltalk

例如,考虑名为Date 的标准类。与Date相对应的是代表该类执行操作的单个对象(称为D )。具体来说,正是D创建了Date类的新对象。因为只有对象才能执行操作(类不能),所以我们实际上不需要为D命名;我们可以简单地使用它所代表的类的名称:

Consider, for example, the standard class named Date. Corresponding to Date is a single object (call it D) that performs operations on behalf of the class. In particular, it is D that creates new objects of class Date. Because only objects execute operations (classes don't), we don't really need a name for D; we can simply use the name of the class it represents:

todaysDate <- 今天的日期

todaysDate <- Date today

此代码使D执行Date类的 today 构造函数,并将对新创建对象的引用赋给名为todaysDate 的变量。

This code causes D to execute the today constructor of class Date, and assigns a reference to the newly created object into a variable named todaysDate.

那么D的类是什么?它显然不是Date,因为D 代表Date类。 Smalltalk 称D是元类 Date 类的一个对象(事实上是唯一的对象) 。 出于技术原因, Date 类也必须由一个对象来表示。 为了避免无限回归,所有代表元类的对象都是名为Metaclass的单个类的实例。 ■

So what is the class of D? It clearly isn't Date, because D represents class Date. Smalltalk says that D is an object (in fact the only object) of the metaclass Date class. For technical reasons, it is also necessary for Date class to be represented by an object. To avoid an infinite regression, all objects that represent metaclasses are instances of a single class named Metaclass. ■

一些历史悠久的语言(尤其是 Modula-3 和 Oberon)根本没有提供构造函数:程序员必须明确地初始化所有内容。Ada 95仅支持对从标准库类型Controlled派生的类型的对象自动调用构造函数和析构函数(InitializeFinalize例程) 。

A few historic languages—notably Modula-3 and Oberon—provided no constructors at all: the programmer had to initialize everything explicitly. Ada 95 supports automatic calls to constructors and destructors (Initialize and Finalize routines) only for objects of types derived from the standard library type Controlled.

10.3.2 引用和值

10.3.2 References and Values

许多面向对象语言(包括 Simula、Smalltalk、Python、Ruby 和 Java)都使用一种编程模型,其中变量引用对象。少数语言(包括 C++ 和 Ada)允许变量具有对象值。Eiffel默认使用引用模型,但允许程序员指定某些类应扩展在这种情况下,这些类的变量将使用值模型。类似地,C# 和 Swift 使用struct来定义变量为值的类型,使用class来定义变量为引用的类型。

Many object-oriented languages, including Simula, Smalltalk, Python, Ruby, and Java, use a programming model in which variables refer to objects. A few languages, including C++ and Ada, allow a variable to have a value that is an object. Eiffel uses a reference model by default, but allows the programmer to specify that certain classes should be expanded, in which case variables of those classes will use a value model. In a similar vein, C# and Swift use struct to define types whose variables are values, and class to define types whose variables are references.

使用变量的引用模型,每个对象都是显式创建的,并且很容易确保调用适当的构造函数。使用变量的值模型,对象创建可以作为细化的结果而隐式发生。在默认情况下不提供对构造函数的自动调用的 Ada 中,细化对象从一开始就未初始化,并且可能会意外尝试在变量具有值之前使用它。在 C++ 中,编译器确保为每个细化对象调用适当的构造函数,但它用于识别构造函数及其参数的规则有时会令人困惑。

With a reference model for variables, every object is created explicitly, and it is easy to ensure that an appropriate constructor is called. With a value model for variables, object creation can happen implicitly as a result of elaboration. In Ada, which doesn't provide automatic calls to constructors by default, elaborated objects begin life uninitialized, and it is possible to accidentally attempt to use a variable before it has a value. In C++, the compiler ensures that an appropriate constructor is called for every elaborated object, but the rules it uses to identify constructors and their arguments can sometimes be confusing.

例 10.27

Example 10.27

C++ 中的声明和构造函数

Declarations and constructors in C++

如果声明一个没有初始值的类类型foo的 C++ 变量,那么编译器将调用foo的零参数构造函数(如果不存在这样的构造函数,但存在其他构造函数,则该声明是一个静态语义错误——调用不存在的子例程):

If a C++ variable of class type foo is declared with no initial value, then the compiler will call foo's zero-argument constructor (if no such constructor exists, but other constructors do, then the declaration is a static semantic error—a call to a nonexistent subroutine):

foo b;      // 调用 foo::foo()

foo b;     // calls foo::foo()

设计与实现

Design & Implementation

10.3 价值/参考权衡

10.3 The value/reference tradeoff

变量的引用模型可以说比值模型更优雅,特别是对于面向对象语言来说,但通常要求从堆中分配对象,并且(在没有编译器优化的情况下)对每次访问都施加额外的间接级别。值模型往往更高效,但难以控制初始化。在具有引用模型的语言(包括 Java)中,一种称为逃逸分析的优化有时可以让编译器确定对给定对象的引用将始终包含在给定方法中(永远不会逃逸)。在这种情况下,可以在方法的堆栈框架中分配对象,从而避免堆分配的开销,更重要的是,避免最终的垃圾收集。

The reference model of variables is arguably more elegant than the value model, particularly for object-oriented languages, but generally requires that objects be allocated from the heap, and imposes (in the absence of compiler optimizations) an extra level of indirection on every access. The value model tends to be more efficient, but makes it difficult to control initialization. In languages with a reference model (including Java), an optimization known as escape analysis can sometimes allow the compiler to determine that references to a given object will always be contained within (will never escape) a given method. In this case the object can be allocated in the method's stack frame, avoiding the overhead of heap allocation and, more significantly, eventual garbage collection.

如果程序员想要调用不同的构造函数,则声明必须指定构造函数参数来驱动重载解析:

If the programmer wants to call a different constructor, the declaration must specify constructor arguments to drive overload resolution:

foo b(10,'x');// 调用 foo::foo(int, char)
foo c{10,'x'};// C++11 中的替代语法

例 10.28

Example 10.28

复制构造函数

Copy constructors

最常见的参数列表由一个对象组成,该对象与被声明的对象类型相同:

The most common argument list consists of a single object, of the same type as the object being declared:

foo一个;
foo b(a);// 调用 foo::foo(foo&)
foo c {a};// 替代语法

通常程序员的意图是声明一个新对象,其初始值与现有对象的初始值“相同”。在这种情况下,写成

Usually the programmer's intent is to declare a new object whose initial value is “the same as” that of the existing object. In this case it may be more natural to write

foo一个;// 调用 foo::foo()
foo b = a;// 调用 foo::foo(foo&)

认识到这一意图,单参数构造函数(匹配类型)有时被称为复制构造函数。重要的是要意识到, b的最新声明中的等号(= )表示初始化,而不是赋值。效果与声明foo b(a)foo b{a}完全相同。它与类似的代码片段不同

In recognition of this intent, a single-argument constructor (of matching type) is sometimes called a copy constructor. It is important to realize that the equals sign (=) in this most recent declaration of b indicates initialization, not assignment. The effect is exactly the same as in the declarations foo b(a) or foo b{a}. It is not the same as in the similar code fragment

foo a,b;// 调用 foo::foo() 两次
b=a;// 调用 foo::operator=(foo&)

这里ab用零参数构造函数初始化,后面使用的等号表示赋值,而不是初始化。这种区别是 C++ 程序中常见的混淆来源。它源于变量值模型和坚持每个精心设计的对象都由构造函数初始化的结合。在使用统一的类类型变量值模型的语言中,规则更简单:如果每个对象都是通过显式调用 new 或其等效函数创建的,则每个这样的调用都提供了调用构造函数的“钩子”。■

Here a and b are initialized with the zero-argument constructor, and the later use of the equals sign indicates assignment, not initialization. The distinction is a common source of confusion in C++ programs. It arises from the combination of a value model of variables and an insistence that every elaborated object be initialized by a constructor. The rules are simpler in languages that use a uniform value model for class-type variables: if every object is created by an explicit call to new or its equivalent, each such call provides the “hook” at which to call a constructor. ■

例 10.29

Example 10.29

临时对象

Temporary objects

在 C++ 中,每个对象都必须构造(同样也必须析构)这一要求不仅适用于具有名称的对象,也适用于临时对象。例如,以下代码需要调用 string (const char*)构造函数和~string()析构函数:

In C++, the requirement that every object be constructed (and likewise destructed) applies not only to objects with names but also to temporary objects. The following, for example, entails a call to both the string(const char*) constructor and the ~string() destructor:

cout << string(“Hi, Mom”).length();      // 打印 7

cout << string(“Hi, Mom”).length();     // prints 7

析构函数在输出语句的末尾被调用:临时对象的行为就好像它的范围只是这里显示的行一样。

The destructor is called at the end of the output statement: the temporary object behaves as if its scope were just the line shown here.

类似地,下面的代码不仅需要调用两次默认字符串构造函数(以初始化ab)和一次调用string::operator+(),还需要调用一次构造函数来初始化由operator+()返回的临时对象——调用者随后会查询该对象的长度:

In a similar vein, the following entails not only two calls to the default string constructor (to initialize a and b) and a call to string::operator+(), but also a constructor call to initialize the temporary object returned by operator+()—the object whose length is then queried by the caller:

字符串a,b;

string a, b;

(a + b).长度();

(a + b).length();

按照函数返回值的惯例,临时对象的空间很可能在调用者的堆栈框架中分配(在静态已知的偏移量处) - 即在调用operator+()length()的例程中。■

As is customary for values returned from functions, the space for the temporary object is likely to be allocated (at a statically known offset) in the stack frame of the caller—that is, in the routine that calls both operator+() and length(). ■

例 10.30

Example 10.30

返回值优化

Return value optimization

现在考虑某个函数f的代码,它返回一个类类型为foo的值。如果foo的实例太大而无法放入寄存器中,则编译器将安排f的调用者传递一个额外的隐藏参数,该参数指定f应构造返回值的位置。如果 return 语句本身创建一个临时对象 -

Now consider the code for some function f, returning a value of class type foo. If instances of foo are too big to fit in a register, the compiler will arrange for f's caller to pass an extra, hidden parameter that specifies the location into which f should construct the return value. If the return statement itself creates a temporary object—

返回 foo( args )

return foo( args )

— 可以在调用者指定的地址轻松构造该对象。但假设f的源代码看起来更像这样:

—that object can easily be constructed at the caller-specified address. But suppose f's source looks more like this:

foo rtn;

foo rtn;

     // 初始化 rtn 字段的复杂代码

     // complex code to initialize the fields of rtn

返回 rtn;

return rtn;

由于我们使用了命名的非临时变量,因此编译器可能需要调用复制构造函数将rtn复制到调用方框架中的位置。4但是,如果其他返回语句没有冲突的需求,也可以从一开始就在调用方指定的位置构造rtn本身,并省略复制操作。此选项称为返回值优化。事实证明,它可以显著提高许多 C++ 程序的性能。

Because we have used a named, non-temporary variable, the compiler may need to invoke a copy constructor to copy rtn into the location in the caller's frame.4 It is also permitted, however (if other return statements don't have conflicting needs), to construct rtn itself at the caller-specified location from the outset, and to elide the copy operation. This option is known as return value optimization. It turns out to significantly improve the performance of many C++ programs.

示例 10.29中,值a + b立即传递给length(),允许编译器使用调用者框架中的同一临时对象作为operator+()的返回值和length()的 this 参数。在其他程序中,编译器可能需要在函数返回后调用复制构造函数:

In Example 10.29, the value a + b was passed immediately to length(), allowing the compiler to use the same temporary object in the caller's frame as both the return value from operator+() and the this argument for length(). In other programs the compiler may need to invoke a copy constructor after a function returns:

foo c;

foo c;

c = f(参数);

c = f( args );

这里,除非编译器能够证明在调用期间不会使用c的值(可能通过别名),否则不能将 c 的位置作为隐藏参数传递给f。底线:在 C++ 中,从函数返回对象可能需要调用零次、一次或两次返回类型的复制构造函数,具体取决于编译器是否能够优化 return 语句和调用者中的后续使用,或者两者兼而有之。■

Here the location of c cannot be passed as the hidden parameter to f unless the compiler is able to prove that c's value will not be used (via an alias, perhaps) during the call. The bottom line: returning an object from a function in C++ may entail zero, one, or two invocations of the return type's copy constructor, depending on whether the compiler is able to optimize either or both of the return statement and the subsequent use in the caller. ■

例 10.31

Example 10.31

Eiffel 构造函数和扩展对象

Eiffel constructors and expanded objects

尽管 Eiffel 既有动态分配的对象,也有扩展的对象,但其构造函数策略稍微简单一些。具体来说,每个变量都初始化为默认值。对于始终扩展的内置类型(整数、浮点数、字符等) ,默认值均为零。对于对对象的引用,默认值为void (null)。对于扩展类类型的变量,默认值以递归方式应用于成员。如上所述,通过调用 Eiffel 的!!创建运算符来创建新对象:

While Eiffel has both dynamically allocated and expanded objects, its strategy with regard to constructors is somewhat simpler. Specifically, every variable is initialized to a default value. For built-in types (integer, floating-point, character, etc.), which are always expanded, the default values are all zero. For references to objects, the default value is void (null). For variables of expanded class types, the defaults are applied recursively to members. As noted above, new objects are created by invoking Eiffel's !! creation operator:

!!var.creator(参数)

!!var.creator(args)

其中var是某个类类型T的变量,而creator是T的构造函数。在常见情况下,var将是一个引用,创建运算符将为类T的对象分配空间,然后调用该对象的构造函数。但是,当T扩展类类型时,允许使用相同的语法,在这种情况下var实际上是一个对象,而不是引用。在这种情况下,!! 操作符只是将已分配的对象(的引用)传递给构造函数。■

where var is a variable of some class type T and creator is a constructor for T. In the common case, var will be a reference, and the creation operator will allocate space for an object of class T and then call the object's constructor. This same syntax is permitted, however, when T is an expanded class type, in which case var will actually be an object, rather than a reference. In this case, the !! operator simply passes to the constructor (a reference to) the already-allocated object. ■

设计与实现

Design & Implementation

10.4 初始化和赋值

10.4 Initialization and assignment

C++ 中的初始化和赋值问题有时会对性能产生令人惊讶的影响,并且可能对程序行为产生影响。如正文所述,“ foo a = b ”可能比“ foo a; a = b ”更有效,并且如果foo的复制构造函数和赋值运算符未设计为语义等效,则可能导致不同的行为。类似的问题可能出现在operator+()operator+=()operator*()operator*=()以及其他类似的操作对上。

Issues around initialization and assignment in C++ can sometimes have a surprising effect on performance—and potentially on program behavior as well. As noted in the body of the text, “foo a = b“ is likely to be more efficient than “foo a; a = b“—and may lead to different behavior if foo's copy constructor and assignment operator have not been designed to be semantically equivalent. Similar issues may arise with operator+() and operator+=(), operator*() and operator*=(), and the other analogous pairs of operations.

进行函数调用时也可能出现类似的问题。按值传递的参数通常会引发对复制构造函数的隐式调用。通过引用传递的参数则不会,而且可能同样可以接受,特别是如果声明为const 的话。(在 C++11 中,值参数也可以使用移动构造函数。)从性能角度来看,复制一个或多个构造函数的成本可能会或可能不会被间接成本以及潜在别名可能抑制代码改进的可能性所抵消。从行为角度来看,由于微小的源代码更改而引起的对不同构造函数和运算符的调用可能是非常细微的错误的根源。C++ 程序员必须非常小心地避免构造函数中的副作用,并确保所有直观等效的方法在实践中具有相同的语义。即便如此,性能权衡可能很难预测。

Similar issues may also arise when making function calls. A parameter that is passed by value typically induces an implicit call to a copy constructor. A parameter that is passed by reference does not, and may be equally acceptable, especially if declared to be const. (In C++11, the value parameter may also use a move constructor.) From a performance perspective, the cost of a copy or more constructor may or may not be outweighed by the cost of indirection and the possibility that code improvement may be inhibited by potential aliases. From a behavioral perspective, calls to different constructors and operators, induced by tiny source code changes, can be a source of very subtle bugs. C++ programmers must exercise great care to avoid side effects in constructors and to ensure that all intuitively equivalent methods have identical semantics in practice. Even then, performance tradeoffs may be very hard to predict.

10.3.3 执行顺序

10.3.3 Execution Order

例 10.32

Example 10.32

基类构造函数参数的规范

Specification of base class constructor arguments

正如我们所见,C++ 坚持每个对象在使用前必须初始化。此外,如果对象的类(称为B)是从其他类(称为A)派生的,则 C++ 坚持在调用B构造函数之前调用A构造函数,这样可以保证派生类永远不会看到其继承的字段处于不一致的状态。当程序员创建B类的对象(通过声明或调用new)时,创建操作会为B构造函数指定参数。这些参数允许 C++ 编译器在存在多个构造函数时解决重载问题。但是编译器从哪里获得A构造函数的参数呢?将它们添加到创建语法中(就像 Simula 所做的那样)显然违反了抽象。C++ 中采用的答案是允许派生类的构造函数的标头指定基类构造函数参数:

As we have seen, C++ insists that every object be initialized before it can be used. Moreover, if the object's class (call it B) is derived from some other class (call it A), C++ insists on calling an A constructor before calling a B constructor, so that the derived class is guaranteed never to see its inherited fields in an inconsistent state. When the programmer creates an object of class B (either via declaration or with a call to new), the creation operation specifies arguments for a B constructor. These arguments allow the C++ compiler to resolve overloading when multiple constructors exist. But where does the compiler obtain arguments for the A constructor? Adding them to the creation syntax (as Simula did) would be a clear violation of abstraction. The answer adopted in C++ is to allow the header of the constructor of a derived class to specify base class constructor arguments:

foo :: foo(foo_params):bar(bar_args){

foo::foo( foo_params ) : bar( bar_args ) {

 

 

这里foo派生自bar。列表foo_params包含此特定foo构造函数的形式参数。在参数列表和子例程定义的左括号之间是对基类bar的构造函数的“调用” 。bar 构造函数的参数可以是涉及foo参数任意复杂表达式。编译器将安排在开始执行foo构造函数之前执行bar构造函数。■

Here foo is derived from bar. The list foo_params consists of formal parameters for this particular foo constructor. Between the parameter list and the opening brace of the subroutine definition is a “call” to a constructor for the base class bar. The arguments to the bar constructor can be arbitrarily complicated expressions involving the foo parameters. The compiler will arrange to execute the bar constructor before beginning execution of the foo constructor. ■

例 10.33

Example 10.33

成员构造函数参数的规范

Specification of member constructor arguments

类似的语法允许 C++ 程序员为类的成员指定构造函数参数或初始值。例如,在图 10.1中,我们可以使用此语法在list_node的构造函数中初始化prevnexthead_nodeval

Similar syntax allows the C++ programmer to specify constructor arguments or initial values for members of the class. In Figure 10.1, for example, we could have used this syntax to initialize prev, next, head_node, and val in the constructor for list_node:

设计与实现

Design & Implementation

10.5 “扩展”对象的初始化

10.5 Initialization of “expanded” objects

C++ 继承了 C 的设计理念,强调执行速度、最小运行时支持和适合“系统”编程,在这种编程中,程序员需要能够编写与汇编语言映射简单明了的代码。因此,在 C++ 中使用变量值模型不仅仅是为了向后兼容 C;它反映了尽可能静态分配变量或在堆栈上分配变量的愿望,以避免动态分配、释放和频繁间接调用的开销。在后面的章节中,我们将看到 C++ 理念的其他几种表现形式,包括手动存储回收(第10.3.4 节)和静态方法绑定(第 10.4.1 节)。

C++ inherits from C a design philosophy that emphasizes execution speed, minimal run-time support, and suitability for “systems” programming, in which the programmer needs to be able to write code whose mapping to assembly language is straightforward and self-evident. The use of a value model for variables in C++ is thus more than an attempt to be backward compatible with C; it reflects the desire to allocate variables statically or on the stack whenever possible, to avoid the overhead of dynamic allocation, deallocation, and frequent indirection. In later sections we shall see several other manifestations of the C++ philosophy, including manual storage reclamation (Section 10.3.4) and static method binding (Section 10.4.1).

list_node() : 上一个(this), 下一个(this), head_node(this), val(0) {

list_node() : prev(this), next(this), head_node(this), val(0) {

 // 空体 -- 没有其他事情可做

 // empty body -- nothing else to do

}

}

鉴于所有这些成员都具有简单(指针或整数)类型,生成的代码不会有显著差异。但假设我们有一些成员本身是某个非平凡类的对象:

Given that all of these members have simple (pointer or integer) types, there will be no significant difference in the generated code. But suppose we have members that are themselves objects of some nontrivial class:

类 foo: bar {

class foo : bar {

 mem1_t member1;      //mem1_t 和

 mem1_t member1;     // mem1_t and

 mem2_t member2;      // mem2_t 是类

 mem2_t member2;     // mem2_t are classes

 …

 …

}

}

foo :: foo(foo_params):bar(bar_args),成员1(mem1_init_val),成员2(mem2_init_val){

foo::foo( foo_params ) : bar( bar_args ), member1(mem1_init_val ), member2( mem2_init_val ) {

 

 

这里,在foo构造函数的头部使用嵌入调用会导致编译器调用成员对象的复制构造函数,而不是调用默认(零参数)构造函数,然后在构造函数主体中调用operator=。因此,语义和性能都可能有所不同。■

Here the use of embedded calls in the header of the foo constructor causes the compiler to call the copy constructors for the member objects, rather than calling the default (zero-argument) constructors, followed by operator= within the body of the constructor. Both semantics and performance may be different as a result. ■

例 10.34

Example 10.34

构造函数转发

Constructor forwarding

当一个构造函数的代码与另一个构造函数的代码非常相似时,C++ 还允许使用成员和基类初始化器语法将一个构造函数转发到另一个构造函数。在示例 10.4中,我们为图 10.1中的list_node类引入了一个新的整数参数构造函数。鉴于这个新构造函数的存在,我们可以将默认(无参数)构造函数重写为

When the code of one constructor closely resembles that of another, C++ also allows the member-and-base-class-initializer syntax to be used to forward one constructor to another. In Example 10.4 we introduced a new integer-parameter constructor for the list_node class of Figure 10.1. Given the existence of this new constructor, we could re-write the default (no-parameter) constructor as

类列表节点{

class list_node {

 

 

 list_node() : list_node(0) { }      // 转发到 (int) 构造函数

 list_node() : list_node(0) { }     // forward to (int) constructor

任何不提供参数的list_node声明现在都将使用参数 0 来调用整数参数构造函数。■

Any declaration of a list_node that does not provide an argument will now call the integer-parameter constructor with an argument of 0. ■

例 10.35

Example 10.35

Java 中基类构造函数的调用

Invocation of base class constructor in Java

与 C++ 一样,Java 也要求在调用派生类的构造函数之前先调用基类的构造函数。不过,语法要简单一些;派生类构造函数代码的初始行可能包含对基类构造函数的“调用”:

Like C++, Java insists that a constructor for a base class be called before the constructor for a derived class. The syntax is a bit simpler, however; the initial line of the code for the derived class constructor may consist of a “call” to the base class constructor:

超级(参数);

super( args );

(C# 具有类似的机制。)如第 10.1 节所述,super是一个 Java 关键字,它引用其所在类的代码中的基类。如果缺少对super的调用,Java 编译器会自动插入对基类的零参数构造函数的调用(在这种情况下必须存在这样的构造函数)。■

(C# has a similar mechanism.) As noted in Section 10.1, super is a Java keyword that refers to the base class of the class in whose code it appears. If the call to super is missing, the Java compiler automatically inserts a call to the base class's zero-argument constructor (in which case such a constructor must exist). ■

因为 Java 对所有对象使用统一的引用模型,所以任何本身是对象的类成员实际上都是引用,而不是“扩展”对象(使用 Eiffel 术语)。Java 只是将这些成员初始化为null。如果程序员想要不同的东西,他或她必须在周围类的构造函数中显式调用new。Smalltalk和(在常见情况下)C# 和 Eiffel 采用类似的方法。在 C# 中,类型为结构体的成员通过将其所有字段设置为零或 null 来初始化。在 Eiffel 中,如果类包含扩展类类型的成员,则该类型需要有一个没有参数的构造函数;Eiffel 编译器会在创建周围对象时安排调用此构造函数。

Because Java uses a reference model uniformly for all objects, any class members that are themselves objects will actually be references, rather than “expanded” objects (to use the Eiffel term). Java simply initializes such members to null. If the programmer wants something different, he or she must call new explicitly within the constructor of the surrounding class. Smalltalk and (in the common case) C# and Eiffel adopt a similar approach. In C#, members whose types are structs are initialized by setting all of their fields to zero or null. In Eiffel, if a class contains members of an expanded class type, that type is required to have a single constructor, with no arguments; the Eiffel compiler arranges to call this constructor when the surrounding object is created.

Smalltalk、Eiffel、CLOS 和 Objective-C 在基类初始化方面都比 C++ 宽松。编译器或解释器会自动调用每个新创建对象的构造函数(创建器、初始化器),但不会自动调用基类的构造函数;它所做的只是将基类数据成员初始化为默认值(零或null)。如果派生类想要不同的行为,其构造函数必须明确调用基类的构造函数。

Smalltalk, Eiffel, CLOS, and Objective-C are all more lax than C++ regarding the initialization of base classes. The compiler or interpreter arranges to call the constructor (creator, initializer) for each newly created object automatically, but it does not arrange to call constructors for base classes automatically; all it does is initialize base class data members to default (zero or null) values. If the derived class wants different behavior, its constructor(s) must call a constructor for the base class explicitly.

10.3.4 垃圾收集

10.3.4 Garbage Collection

例 10.36

Example 10.36

使用析构函数回收空间

Reclaiming space with destructors

当销毁 C++ 对象时,首先调用派生类的析构函数,然后按与派生类相反的顺序调用基类的析构函数。迄今为止,C++ 中析构函数最常见的用途是手动存储回收。再次考虑图 10.8中的队列类。因为我们的队列派生自图 10.2中的列表,所以它的默认析构函数将调用显式的~list析构函数,如果列表(即队列)非空,它将抛出异常。假设我们希望允许销毁非空队列,并简单地清理其空间。由于队列节点由enqueue创建,并且只在队列本身的代码中使用,我们可以安全地安排队列的析构函数删除任何剩余的节点:

When a C++ object is destroyed, the destructor for the derived class is called first, followed by those of the base class(es), in reverse order of derivation. By far the most common use of destructors in C++ is manual storage reclamation. Consider again the queue class of Figure 10.8. Because our queue is derived from the list of Figure 10.2, its default destructor will call the explicit ~list destructor, which will throw an exception if the list (i.e., the queue) is nonempty. Suppose instead that we wish to allow the destruction of a nonempty queue, and simply clean up its space. Since queue nodes are created by enqueue, and are used only within the code of the queue itself, we can safely arrange for the queue's destructor to delete any nodes that remain:

~队列() {

~queue() {

 当(!空()){

 while (!empty()) {

  列表节点* p = 内容.head();

  list_node* p = contents.head();

  p->删除();

  p->remove();

  删除p;

  delete p;

 }

 }

}

}

或者,由于 dequeue 已经被设计为删除包含出队元素的节点:

Alternatively, since dequeue has already been designed to delete the node that contained the dequeued element:

~队列() {

~queue() {

 当(!空()){

 while (!empty()) {

  int v = 出队();

  int v = dequeue();

 }

 }

}

}

在现代 C++ 代码中,存储管理通常通过使用智能指针第 8.5.3 节)来实现。这些指针在指针的析构函数中排列,以确定是否还有其他指向同一对象的指针继续存在——如果不存在,则回收指向的对象。■

In modern C++ code, storage management is often facilitated through the use of smart pointers (Section 8.5.3). These arrange, in the destructor for a pointer, to determine whether any other pointers to the same object continue to exist—and if not, to reclaim that pointed-to object. ■

在具有自动垃圾收集功能的语言中,对析构函数的需求要小得多。事实上,在具有垃圾收集功能的语言中,整个析构概念都是值得怀疑的,因为程序员几乎无法控制对象何时被销毁。Java 和 C# 允许程序员声明一个finalize方法,该方法将在垃圾收集器回收对象空间之前立即调用,但该功能并未得到广泛使用。

In languages with automatic garbage collection, there is much less need for destructors. In fact, the entire idea of destruction is suspect in a garbage-collected language, because the programmer has little or no control over when an object is going to be destroyed. Java and C# allow the programmer to declare a finalize method that will be called immediately before the garbage collector reclaims the space for an object, but the feature is not widely used.

10-01-9780124104099检查你的理解

Check Your Understanding

23. 构造函数会为对象分配空间吗?解释一下。

23. Does a constructor allocate space for an object? Explain.

24. Smalltalk 中的 元类是什么?

24. What is a metaclass in Smalltalk?

25. 为什么在具有变量引用模型(而不是值模型)的语言中对象初始化更简单?

25. Why is object initialization simpler in a language with a reference model of variables (as opposed to a value model)?

26.  C++(或 Java 或 C#)编译器如何判断给定对象应使用哪个构造函数?Eiffel 和 Smalltalk 的答案有何不同?

26. How does a C++ (or Java or C#) compiler tell which constructor to use for a given object? How does the answer differ for Eiffel and Smalltalk?

27. 什么是逃逸分析?描述为什么它在具有变量参考模型的语言中很有用。

27. What is escape analysis? Describe why it might be useful in a language with a reference model of variables.

28. 总结一下 C++ 中确定类、其基类及其字段的类的构造函数调用顺序的规则。其他语言中如何简化这些规则?

28. Summarize the rules in C++ that determine the order in which constructors are called for a class, its base class(es), and the classes of its fields. How are these rules simplified in other languages?

29. 解释C++中初始化和赋值的区别。

29. Explain the difference between initialization and assignment in C++.

30. 为什么 C++ 比 Eiffel 更需要析构函数?

30. Why does C++ need destructors more than Eiffel does?

10.4 动态方法绑定

10.4 Dynamic Method Binding

继承/类型扩展的主要后果之一是派生类D拥有其基类C的所有成员(数据和子程序)。只要D不隐藏C的任何公开可见成员(参见练习 10.15 ),允许在任何需要 C 类对象的上下文中使用类D的对象都是有意义的:我们对C对象所做的任何操作,我们都可以对类D的对象执行。换句话说,不隐藏其基类的任何公开可见成员的派生类是该基类的子类型。

One of the principal consequences of inheritance/type extension is that a derived class D has all the members—data and subroutines—of its base class C. As long as D does not hide any of the publicly visible members of C (see Exercise 10.15), it makes sense to allow an object of class D to be used in any context that expects an object of class C: anything we might want to do to an object of class C we can also do to an object of class D. In other words, a derived class that does not hide any publicly visible members of its base class is a subtype of that base class.

例 10.37

Example 10.37

基类上下文中的派生类对象

Derived class objects in a base class context

在需要基类的上下文中使用派生类的能力称为子类型多态性。如果我们想象一个大学的行政计算系统,我们可以从类person派生类studentprofessor

The ability to use a derived class in a context that expects its base class is called subtype polymorphism. If we imagine an administrative computing system for a university, we might derive classes student and professor from class person:

班级人 { …

class person { …

班级学生:公众人物{…

class student : public person { …

班级教授:公众人物 {...

class professor : public person { …

因为无论是学生还是教授对象具有人员对象的所有属性,我们应该能够在人员上下文中使用它们:

Because both student and professor objects have all the properties of a person object, we should be able to use them in a person context:

学生;

student s;

p 教授;

professor p;

人 *x = &s;

person *x = &s;

人 *y = &p;

person *y = &p;

此外,还有如下子程序

Moreover a subroutine like

void person::print_mailing_label() { …

void person::print_mailing_label() { …

将是多态的——能够接受多种类型的参数:

would be polymorphic—capable of accepting arguments of multiple types:

打印邮件标签();// 即 print_mailing_label(s)
p.打印邮件标签();// 即 print_mailing_label(p)

与其他形式的多态性一样,我们依赖于这样一个事实:print_mailing_label只使用其形式参数中所有实际参数所共有的特征。■

As with other forms of polymorphism, we depend on the fact that print_mailing_label uses only those features of its formal parameter that all actual parameters will have in common. ■

例 10.38

Example 10.38

静态和动态方法绑定

Static and dynamic method binding

但现在假设我们在两个派生类中都重新定义了print_mailing_label 。例如,我们可能想在标签的一角编码某些信息(学生的年级、教授的所在院系)。现在我们有了子程序的多个版本—— student::print_mailing_labelprofessor::print_mailing_label,而不是单一的多态person::print_mailing_label。我们将获得哪个版本取决于对象:

But now suppose that we have redefined print_mailing_label in each of the two derived classes. We might, for example, want to encode certain information (student's year in school, professor's home department) in the corner of the label. Now we have multiple versions of our subroutine—student::print_mailing_label and professor::print_mailing_label, rather than the single, polymorphic person::print_mailing_label. Which version we will get depends on the object:

打印邮件标签();// 学生::打印邮寄标签
p.打印邮件标签();// 教授::print_mailing_label(p)

但是

But what about

x->打印邮件标签();//??
y->打印邮件标签();//??

选择要调用的方法是否取决于变量 xy的类型,或者这些变量引用的对象 sp 的类?■

Does the choice of the method to be called depend on the types of the variables x and y, or on the classes of the objects s and p to which those variables refer? ■

第一个选项(使用引用的类型)称为静态方法绑定。第二个选项(使用对象的类)称为动态方法绑定。动态方法绑定是面向对象编程的核心。例如,想象一下我们的管理计算程序创建了一个图书馆图书逾期未还的人员名单。该名单可能包含学生教授。如果我们遍历该列表并为每个打印邮寄标签,动态方法绑定将确保为每个人调用正确的打印例程。在这种情况下,派生类中的定义被称为覆盖基类中的定义。

The first option (use the type of the reference) is known as static method binding. The second option (use the class of the object) is known as dynamic method binding. Dynamic method binding is central to object-oriented programming. Imagine, for example, that our administrative computing program has created a list of persons who have overdue library books. The list may contain both students and professors. If we traverse the list and print a mailing label for each person, dynamic method binding will ensure that the correct printing routine is called for each individual. In this situation the definitions in the derived classes are said to override the definition in the base class.

语义和性能

Semantics and Performance

例 10.39

Example 10.39

动态绑定的必要性

The need for dynamic binding

反对静态方法绑定(因而支持基于引用对象类型的动态绑定)的主要论点是,静态方法否认派生类对其自身状态一致性的控制。例如,假设我们正在构建一个包含 text_file 类的 I/O

The principal argument against static method binding—and thus in favor of dynamic binding based on the type of the referenced object—is that the static approach denies the derived class control over the consistency of its own state. Suppose, for example, that we are building an I/O library that contains a text_file class:

类文本文件{

class text_file {

 字符*名称;

 char *name;

 长位置;      //文件指针

 long position;     // file pointer

民众:

public:

 无效寻求(长何时);

 void seek(long whence);

 …

 …

};

};

现在假设我们有一个派生类read_ahead_text_file

Now suppose we have a derived class read_ahead_text_file:

类 read_ahead_text_file:公共文本文件{

class read_ahead_text_file : public text_file {

 char *即将到来的字符;

 char *upcoming_characters;

民众:

public:

 void seek(long whence);      // 重新定义

 void seek(long whence);     // redefinition

 …

 …

};

};

read_ahead_text_file::seek 的代码无疑需要更改缓存的coming_characters的值。但是,如果该方法不是动态分派的,我们就不能保证会发生这种情况:如果我们将read_ahead_text_file引用传递给需要text_file引用作为参数的子例程,并且该子例程随后调用seek ,我们将获得基类中的seek版本。■

The code for read_ahead_text_file::seek will undoubtedly need to change the value of the cached upcoming_characters. If the method is not dynamically dispatched, however, we cannot guarantee that this will happen: if we pass a read_ahead_text_file reference to a subroutine that expects a text_file reference as argument, and if that subroutine then calls seek, we'll get the version of seek in the base class. ■

不幸的是,正如我们将在第 10.4.3 节中看到的,动态方法绑定会产生运行时开销。虽然这种开销通常不大,但对于性能至关重要的应用程序中的小子程序来说,这仍然是一个问题。Smalltalk、Objective-C、Python 和 Ruby 对所有方法都使用动态方法绑定。Java 和 Eiffel 默认使用动态方法绑定,但允许将各个方法和(在 Java 中)类标记为final(Java)或freeze(Eiffel),在这种情况下,它们不能被派生类覆盖,因此可以采用优化的实现。Simula、C++、C# 和 Ada 95 默认使用静态方法绑定,但允许程序员在需要时指定动态绑定。在后一种语言中,区分覆盖使用动态绑定的方法和(仅仅)重新定义使用静态绑定的方法是常用的术语。为了清楚起见,每当派生类中的方法覆盖或重新定义基类中同名的方法时, C# 都要求明确使用关键字overridenew。Java和 C++11 具有类似的注释,鼓励但不要求使用它们。

Unfortunately, as we shall see in Section 10.4.3, dynamic method binding imposes run-time overhead. While this overhead is generally modest, it is nonetheless a concern for small subroutines in performance-critical applications. Smalltalk, Objective-C, Python, and Ruby use dynamic method binding for all methods. Java and Eiffel use dynamic method binding by default, but allow individual methods and (in Java) classes to be labeled final (Java) or frozen (Eiffel), in which case they cannot be overridden by derived classes, and can therefore employ an optimized implementation. Simula, C++, C#, and Ada 95 use static method binding by default, but allow the programmer to specify dynamic binding when desired. In these latter languages it is common terminology to distinguish between overriding a method that uses dynamic binding and (merely) redefining a method that uses static binding. For the sake of clarity, C# requires explicit use of the keywords override and new whenever a method in a derived class overrides or redefines (respectively) a method of the same name in a base class. Java and C++11 have similar annotations whose use is encouraged but not required.

10.4.1 虚拟方法和非虚拟方法

10.4.1 Virtual and Nonvirtual Methods

例 10.40

Example 10.40

C++ 和 C# 中的虚方法

Virtual methods in C++ and C#

在 Simula、C++ 和 C# 中,默认情况下使用静态方法绑定,程序员可以通过将特定方法标记为虚拟来指定它们应使用动态绑定。对虚拟方法的调用在运行时根据对象的类(而不是引用的类型)分派到适当的实现。在 C++ 和 C# 中,关键字virtual是子例程声明的前缀:5

In Simula, C++, and C#, which use static method binding by default, the programmer can specify that particular methods should use dynamic binding by labeling them as virtual. Calls to virtual methods are dispatched to the appropriate implementation at run time, based on the class of the object, rather than the type of the reference. In C++ and C#, the keyword virtual prefixes the subroutine declaration:5

班级人{

class person {

民众:

public:

 虚拟void print_mailing_label();

 virtual void print_mailing_label();

 

 

例 10.41

Example 10.41

Ada 95 中的类范围类型

Class-wide types in Ada 95

Ada 95 采用了不同的方法。Ada 95 程序员不会将动态调度与特定方法关联,而是将其与某些引用关联。在我们的邮件标签示例中,可以将形式参数或访问变量(指针)声明为类范围的类型person 'Class,在这种情况下,对该参数或变量的所有方法的所有调用都将根据其引用的对象的类进行调度:

Ada 95 adopts a different approach. Rather than associate dynamic dispatch with particular methods, the Ada 95 programmer associates it with certain references. In our mailing label example, a formal parameter or an access variable (pointer) can be declared to be of the class-wide type person 'Class, in which case all calls to all methods of that parameter or variable will be dispatched based on the class of the object to which it refers:

类型人被标记记录…

type person is tagged record …

类型的学生是新来的……

type student is new person with …

类型教授是新人,有……

type professor is new person with …

程序 print_mailing_label(r: person) 是……

procedure print_mailing_label(r : person) is …

程序 print_mailing_label(s: student) 是…

procedure print_mailing_label(s : student) is …

程序 print_mailing_label(p: professor) 是…

procedure print_mailing_label(p : professor) is …

程序 print_appropriate_label(r : person'Class) 是

procedure print_appropriate_label(r : person'Class) is

开始

begin

 打印邮寄标签(r);

 print_mailing_label(r);

 ——调用适当的重载版本,取决于

 -- calls appropriate overloaded version, depending

 -- 运行时 r 的类型

 -- on type of r at run time

结束打印适当标签;

end print_appropriate_label;

10.4.2 抽象类

10.4.2 Abstract Classes

例 10.42

Example 10.42

Java 和 C# 中的抽象方法

Abstract methods in Java and C#

在大多数面向对象语言中,可以省略基类中的虚拟方法体。在 Java 和 C# 中,可以通过将类和缺少的方法都标记为abstract来实现这一点:

In most object-oriented languages it is possible to omit the body of a virtual method in a base class. In Java and C#, one does so by labeling both the class and the missing method as abstract:

抽象类人{

abstract class person {

 

 

 公共抽象void print_mailing_label();

 public abstract void print_mailing_label();

 

 

例 10.43

Example 10.43

C++ 中的抽象方法

Abstract methods in C++

C++ 中的符号不​​太直观:在子程序声明后将“赋值”为零:

The notation in C++ is somewhat less intuitive: one follows the subroutine declaration with an “assignment” to zero:

班级人{

class person {

 …

 …

民众:

public:

 虚拟void print_mailing_label()=0;

 virtual void print_mailing_label() = 0;

 …

 …

C++ 将抽象方法称为纯虚方法。■

C++ refers to abstract methods as pure virtual methods. ■

无论声明语法如何,如果一个至少有一个抽象方法,则该类被称为抽象类。无法声明抽象类的对象,因为它至少缺少一个成员。抽象类的唯一目的是作为其他具体类的基类。具体类(或其中间祖先之一)必须为其继承的每个抽象方法提供实际定义。基类中抽象方法的存在为动态方法绑定提供了“钩子”;它允许程序员编写调用基类对象的方法(引用)的代码,前提是适当的具体方法将在运行时被调用。除了抽象方法之外没有其他成员的类(没有字段或方法体)在 Java、C# 和 Ada 2005 中称为接口。它们支持受限制的“混合”形式的多重继承,我们将在10.5 节中讨论。6

Regardless of declaration syntax, a class is said to be abstract if it has at least one abstract method. It is not possible to declare an object of an abstract class, because it would be missing at least one member. The only purpose of an abstract class is to serve as a base for other, concrete classes. A concrete class (or one of its intermediate ancestors) must provide a real definition for every abstract method it inherits. The existence of an abstract method in a base class provides a “hook” for dynamic method binding; it allows the programmer to write code that calls methods of (references to) objects of the base class, under the assumption that appropriate concrete methods will be invoked at run time. Classes that have no members other than abstract methods—no fields or method bodies—are called interfaces in Java, C#, and Ada 2005. They support a restricted, “mix-in” form of multiple inheritance, which we will consider in Section 10.5.6

10.4.3 会员查找

10.4.3 Member Lookup

例 10.44

Example 10.44

虚表

Vtables

使用静态方法绑定(如在 Simula、C++、C# 或 Ada 95 中),编译器始终可以根据所用变量的类型判断要调用方法的哪个版本。但是,使用动态方法绑定,引用或指针变量所引用的对象必须包含足够的信息,以便编译器生成的代码能够在运行时找到方法的正确版本。最常见的实现是用一条记录表示每个对象,该记录的第一个字段包含对象类的虚方法表( vtable ) 的地址(参见图 10.3)。vtable 是一个数组,其第i个条目表示对象的第 i个虚方法的代码的地址。给定具体类的所有对象共享同一个 vtable。■

With static method binding (as in Simula, C++, C#, or Ada 95), the compiler can always tell which version of a method to call, based on the type of the variable being used. With dynamic method binding, however, the object referred to by a reference or pointer variable must contain sufficient information to allow the code generated by the compiler to find the right version of the method at run time. The most common implementation represents each object with a record whose first field contains the address of a virtual method table (vtable) for the object's class (see Figure 10.3). The vtable is an array whose ith entry indicates the address of the code for the object's ith virtual method. All objects of a given concrete class share the same vtable. ■

f10-04-9780124104099
图 10.3 虚方法的实现。对象F的表示 以类foo的 vtable 地址开头。(此类的所有对象都将指向同一个 vtable。)vtable 本身由一个地址数组组成,每个地址用于类的每个虚拟方法的代码。F 的其余部分由其字段的表示组成。

例 10.45

Example 10.45

虚拟方法调用的实现

Implementation of a virtual method call

假设方法的this (自身)指针在寄存器r1中传递,m是类foo的第三个方法,f是指向类foo对象的指针。那么调用f->m()的代码如下所示:

Suppose that the this (self) pointer for methods is passed in register r1, that m is the third method of class foo, and that f is a pointer to an object of class foo. Then the code to call f->m() looks something like this:

r1:= f
r2 := *r1–– vtable 地址
r2 := *(r2 + (3−1) × 4)–– 假设 4 = sizeof (地址)
调用*r2

在典型的现代机器上,此调用序列比对静态标识方法的调用长两个指令(两者都访问内存)。只要编译器可以在编译时推断出相关对象的类型,就可以避免额外的开销。对于对对象值变量(而不是引用和指针)的方法的调用,推断是微不足道的。■

On a typical modern machine this calling sequence is two instructions (both of which access memory) longer than a call to a statically identified method. The extra overhead can be avoided whenever the compiler can deduce the type of the relevant object at compile time. The deduction is trivial for calls to methods of object-valued variables (as opposed to references and pointers). ■

例 10.46

Example 10.46

单继承的实现

Implementation of single inheritance

如果 bar 派生自foo,我们将其附加字段放在表示它的“记录”的末尾。我们通过复制foo的 vtable 为bar创建 vtable,替换bar重写的任何虚拟方法的条目,并附加bar中引入的任何虚拟方法的条目(参见图 10.4 )。如果我们有一个bar类的对象,我们可以安全地将其地址分配给foo*类型的变量:

If bar is derived from foo, we place its additional fields at the end of the “record” that represents it. We create a vtable for bar by copying the vtable for foo, replacing the entries of any virtual methods overridden by bar, and appending entries for any virtual methods introduced in bar (see Figure 10.4). If we have an object of class bar we can safely assign its address into a variable of type foo*:

f10-05-9780124104099
图 10.4 单继承的实现。如图10.3所示,对象B的表示以其类的 vtable 的地址开始。表中的前四个条目表示的成员与foo相同,但有一个 — m — 已被覆盖,现在包含不同子例程的代码地址。bar 的其他字段跟在B表示中从foo继承的字段之后;类bar的 vtable 中其他虚拟方法跟在从foo继承的字段之后。

类 foo { …

class foo { …

类 bar:公共 foo {…

class bar : public foo { …

foo F;

foo F;

酒吧B;

bar B;

foo* q;

foo* q;

酒吧* s;

bar* s;

q = &B;      // ok;通过 q 的引用将使用前缀

q = &B;     // ok; references through q will use prefixes

          // B 的数据空间和 vtable

          // of B's data space and vtable

s = &F;      // 静态语义错误;F 缺少附加

s = &F;     // static semantic error; F lacks the additional

          // 条形的数据和 vtable 条目

          // data and vtable entries of a bar

在 C++ 中(与所有静态类型的面向对象语言一样),编译器可以静态地验证此代码的类型正确性。它可能不知道q引用的对象在运行时是什么类,但它知道它要么是foo,要么是(直接或间接)从foo派生的某个类,这确保它将具有foo特定代码可能访问的所有成员。■

In C++ (as in all statically typed object-oriented languages), the compiler can verify the type correctness of this code statically. It may not know what the class of the object referred to by q will be at run time, but it knows that it will either be foo or something derived (directly or indirectly) from foo, and this ensures that it will have all the members that maybe accessed by foo-specific code. ■

例 10.47

Example 10.47

C++ 中的强制类型转换

Casts in C++

C++ 允许通过dynamic_cast运算符进行“向后”赋值:

C++ allows “backward” assignments by means of a dynamic_cast operator:

s = dynamic_cast<bar*>(q);// 执行运行时检查

如果运行时检查失败,则为s分配一个空指针。为了向后兼容,C++ 还支持对象指针和引用的传统 C 样式转换:

If the run-time check fails, s is assigned a null pointer. For backward compatibility C++ also supports traditional C-style casts of object pointers and references:

s = (条形码) q;// 允许,但有风险

使用C 风格的强制类型转换时,程序员需要确保所涉及的实际对象属于适当的类型:不执行动态语义检查。■

With a C-style cast it is up to the programmer to ensure that the actual object involved is of an appropriate type: no dynamic semantic check is performed. ■

例 10.48

Example 10.48

Eiffel 和 C# 中的反向赋值

Reverse assignment in Eiffel and C#

Java 和 C# 使用传统的强制类型转换符号,但执行动态检查。Eiffel 有一个反向赋值运算?=,它(与 C++ dynamic_cast一样)当且仅当运行时的类型可接受时,才会将对象引用赋给变量:

Java and C# employ the traditional cast notation, but perform the dynamic check. Eiffel has a reverse assignment operator, ?=, which (like the C++ dynamic_cast) assigns an object reference into a variable if and only if the type at run time is acceptable:

设计与实现

Design & Implementation

10.6 反向赋值

10.6 Reverse assignment

Eiffel、Java、C# 和 C++ 的实现通过在每个 vtable 中包含运行时类型描述符的地址来支持对反向赋值的动态检查。在 C++ 中,dynamic_cast仅允许用于多态类型(具有虚拟方法的类)的指针和引用,因为非多态类型的对象没有 vtable。单独的static_cast操作可用于非多态类型,但它不执行运行时检查,因此当应用于派生类类型的指针时本质上是不安全的。

Implementations of Eiffel, Java, C#, and C++ support dynamic checks on reverse assignment by including in each vtable the address of a run-time type descriptor. In C++, dynamic_cast is permitted only on pointers and references of polymorphic types (classes with virtual methods), since objects of nonpolymorphic types do not have vtables. A separate static_cast operation can be used on nonpolymorphic types, but it performs no run-time check, and is thus inherently unsafe when applied to a pointer of a derived class type.

类 foo …

class foo …

类 bar 继承 foo…

class bar inherit foo …

f : foo

f : foo

b :条形图

b : bar

f := b      -- 总是可以的

f := b     -- always ok

b ?= f      -- 反向赋值:如果 f 引用 bar 对象,则 b 获取 f

b ?= f     -- reverse assignment: b gets f if f refers to a bar object

          —— 在运行时;否则 b 将变为 void

          -- at run time; otherwise b gets void

C# 提供了执行类似功能的 as 运算符。■

C# provides an as operator that performs a similar function. ■

如第 7.3 节所述,Smalltalk 采用“鸭子类型”:变量是无类型的引用,对任何对象的引用都可以赋值给任何变量。只有当代码在运行时实际尝试调用操作(发送“消息”)时,语言实现才会检查对象是否支持该操作;如果支持,则认为对象的类型是可以接受的。实现很简单:对象的字段永远不会公共;方法提供了对象交互的唯一方式。对象的表示以类型描述符的地址开始。类型描述符包含一个将方法名称映射到代码片段的字典。在运行时,Smalltalk 解释器在字典中执行查找操作以查看是否支持该方法。如果不支持,它会生成“消息无法理解”错误 - 相当于 Lisp 中的类型冲突错误。CLOS、Objective-C、Swift 和面向对象的脚本语言提供类似的语义,并邀请类似的实现。动态方法可以说比静态方法更灵活,但当方法较小时,它会带来显着的成本,并延迟错误报告。

As noted in Section 7.3, Smalltalk employs “duck typing”: variables are untyped references, and a reference to any object may be assigned into any variable. Only when code actually attempts to invoke an operation (send a “message”) at run time does the language implementation check to see whether the operation is supported by the object; if so, the object's type is assumed to be acceptable. The implementation is straightforward: fields of an object are never public; methods provide the only means of object interaction. The representation of an object begins with the address of a type descriptor. The type descriptor contains a dictionary that maps method names to code fragments. At run time, the Smalltalk interpreter performs a lookup operation in the dictionary to see if the method is supported. If not, it generates a “message not understood” error—the equivalent of a type-clash error in Lisp. CLOS, Objective-C, Swift, and the object-oriented scripting languages provide similar semantics, and invite similar implementations. The dynamic approach is arguably more flexible than the static, but it imposes significant cost when methods are small, and delays the reporting of errors.

设计与实现

Design & Implementation

10.7 脆弱基类问题

10.7 The fragile base class problem

在某些情况下,即使语言允许编译时查找,在运行时执行方法查找也是可取的。例如,在 Java 中,动态查找(或“即时”编译)可以帮助避免脆弱基类问题的重要实例,在这种情况下,对基类的看似无害的更改可能会破坏派生类的行为。

Under certain circumstances, it can be desirable to perform method lookup at run time even when the language permits compile-time lookup. In Java, for example, dynamic lookup (or “just-in-time” compilation) can help to avoid important instances of the fragile base class problem, in which seemingly benign changes to a base class may break the behavior of a derived class.

Java 实现依赖于大型标准库的存在。该库预计会随着时间的推移而发展。尽管设计人员可能会小心地最大限度地提高向后兼容性(很少甚至根本不会删除类的任何成员),但旧版本库的用户可能会偶尔尝试运行为新版本库编写的代码。在这种情况下,依赖有关库类表示的静态假设将是灾难性的:试图使用新添加的库功能的代码最终可能会访问可用表示末尾以外的内存。相比之下,运行时方法查找(或针对当前可用版本的库执行的编译)将生成有用的“未在您的类版本中找到成员”动态错误消息。

Java implementations depend on the presence of a large standard library. This library is expected to evolve over time. Though the designers will presumably be careful to maximize backward compatibility—seldom if ever deleting any members of a class—it is likely that users of old versions of the library will on occasion attempt to run code that was written with a new version of the library in mind. In such a situation it would be disastrous to rely on static assumptions about the representation of library classes: code that tries to use a newly added library feature could end up accessing memory beyond the end of the available representation. Run-time method lookup, by contrast (or compilation performed against the currently available version of the library), will produce a helpful “member not found in your version of the class” dynamic error message.

可以使用各种其他技术来防范脆弱基类问题。例如,在 Objective-C 中,对库类的修改通常采用单独编译的扩展(称为类别)的形式该扩展在运行时加载到程序中。加载机制会更新运行时系统执行动态方法查找的字典。如果没有类别,尝试使用新功能将自动引发“未找到方法”错误。

A variety of other techniques can be used to guard against aspects of the fragile base class problem. In Objective-C, for example, modifications to a library class typically take the form of a separately compiled extension called a category, which is loaded into a program at run time. The loading mechanism updates the dictionary in which the runtime system performs dynamic method lookup. Without the category, attempts to use the new functionality will automatically elicit a “method not found” error.

除了增加间接开销之外,虚拟方法通常还会妨碍编译时内联扩展子例程。当子例程较小且调用频繁时,缺少内联子例程可能会带来严重的性能问题。与 C 一样,C++ 尽可能避免运行时开销:因此它默认使用静态方法绑定,并且严重依赖对象值变量,因此甚至可以在编译时分派虚拟方法。

In addition to imposing the overhead of indirection, virtual methods often preclude the in-line expansion of subroutines at compile time. The lack of in-line subroutines can be a serious performance problem when subroutines are small and frequently called. Like C, C++ attempts to avoid run-time overhead whenever possible: hence its use of static method binding as the default, and its heavy reliance on object-valued variables, for which even virtual methods can be dispatched at compile time.

10.4.4 对象闭包

10.4.4 Object Closures

例 10.49

Example 10.49

对象闭包中的虚方法

Virtual methods in an object closure

我们已经指出(在第 3.6.4 节和其他地方),对象闭包可用于面向对象语言,以实现与具有嵌套子例程的语言中的子例程闭包大致相同的效果 - 即封装具有上下文的方法以供以后执行。应该注意的是,此机制的完全通用性依赖于动态方法绑定。回想一下示例 3.36中的plus_x对象闭包,这里改编为示例 9.23中的apply_to_A代码,并以通用形式重写:

We have noted (in Section 3.6.4 and elsewhere) that object closures can be used in an object-oriented language to achieve roughly the same effect as subroutine closures in a language with nested subroutines—namely, to encapsulate a method with context for later execution. It should be noted that this mechanism relies, for its full generality, on dynamic method binding. Recall the plus_x object closure from Example 3.36, here adapted to the apply_to_A code of Example 9.23, and rewritten in generic form:

模板<类型名称 T>

template<typename T>

类 un_op {

class un_op {

民众:

public:

 虚拟 T 运算符()(T i)const = 0;

 virtual T operator()(T i) const = 0;

};

};

类 plus_x :公共 un_op<int> {

class plus_x : public un_op<int> {

 常量 int x;

 const int x;

民众:

public:

 plus_x (int n) : x (n) { }

 plus_x(int n) : x(n) { }

 虚拟 int 运算符()(int i)const { 返回 i + x; }

 virtual int operator()(int i) const { return i + x; }

};

};

void apply_to_A(const un_op<int>& f, int A[], int A_size) {

void apply_to_A(const un_op<int>& f, int A[], int A_size) {

 int 我;

 int i;

 对于 (i = 0; i < A_size; i++) A[i] = f(A[i]);

 for (i = 0; i < A_size; i++) A[i] = f(A[i]);

}

}

int A[10];

int A[10];

应用于A(plus_x(2),A,10);

apply_to_A(plus_x(2), A, 10);

任何从un_op<int>派生的对象都可以传递给apply_to_A 。由于operator()是虚拟的,因此始终会调用“正确的”函数。■

Any object derived from un_op<int> can be passed to apply_to_A. The “right” function will always be called because operator() is virtual. ■

例 10.50

Example 10.50

封装参数

Encapsulating arguments

对于许多应用程序来说,将方法及其参数封装在对象闭包中以供稍后执行是一种特别有用的习惯用法。例如,假设我们正在编写离散事件模拟,如第C-9.5.4 节所述。我们可能需要一种通用机制,使我们能够安排在未来某个时间点调用任意子例程,并使用任意一组参数。如果我们想要调用的子例程在参数数量和类型上有所不同,则我们将无法将它们传递给通用的schedule_at 例程。我们可以使用对象闭包来解决这个问题,如图10.5所示。这种技术非常常见,C++11 通过标准库例程支持它。图 10.5中的fn_callcall_foo类可以在 C++11 中省略。然后,函数schedule_at将被定义为接受std::function<void()>类的对象(封装要使用零个参数调用的函数的函数对象)作为其第一个参数。图 10.5在第一个参数位置传递的对象cf将被声明为

A particularly useful idiom for many applications is to encapsulate a method and its arguments in an object closure for later execution. Suppose, for example, that we are writing a discrete event simulation, as described in Section C-9.5.4. We might like a general mechanism that allows us to schedule a call to an arbitrary subroutine, with an arbitrary set of parameters, to occur at some future point in time. If the subroutines we want to have called vary in their numbers and types of parameters, we won't be able to pass them to a general-purpose schedule_at routine. We can solve the problem with object closures, as shown in Figure 10.5. This technique is sufficiently common that C++11 supports it with standard library routines. The fn_call and call_foo classes of Figure 10.5 could be omitted in C++11. Function schedule_at would then be defined to take an object of class std::function<void()> (function object encapsulating a function to be called with zero arguments) as its first parameter. Object cf, which Figure 10.5 passes in that first parameter position, would be declared as

f10-06-9780124104099
图 10.5 子程序指针和虚拟方法。类call_foo封装了子程序指针和要传递给子程序的值。它导出一个无参数的子程序,可用于触发封装的调用。

std::function<void()> cf = std::bind(foo, 3, 3.14, 'x');

std::function<void()> cf = std::bind(foo, 3, 3.14, 'x');

bind例程(自动实例化的通用函数)将其第一个参数(函数)与最终应传递给该函数的参数封装在一起。标准库甚至提供了一个“占位符”机制(此处未显示),允许程序员仅绑定函数参数的子集,以便最终传递给函数对象的参数可用于填充剩余位置。■

The bind routine (an automatically instantiated generic function) encapsulates its first parameter (a function) together with the arguments that should eventually be passed to that function. The standard library even provides a “placeholder” mechanism (not shown here) that allows the programmer to bind only a subset of the function's parameters, so that parameters eventually passed to the function object can be used to fill in the remaining positions. ■

对象闭包在 Java(以及其他几种语言)中被广泛用于封装新创建的控制线程的启动参数(有关详细信息,请参阅第 13.2.3 节)。它们还可用于(如探索 6.46中所述)通过访问者模式实现迭代器。

Object closures are commonly used in Java (and several other languages) to encapsulate start-up arguments for newly created threads of control (more on this in Section 13.2.3). They can also be used (as noted in Exploration 6.46) to implement iterators via the visitor pattern.

10-01-9780124104099检查你的理解

Check Your Understanding

31. 解释动态和静态方法绑定之间的区别(即虚拟方法非虚拟方法之间的区别)。

31. Explain the difference between dynamic and static method binding (i.e., between virtual and nonvirtual methods).

32. 总结动态方法绑定的基本论点。为什么 C++ 和 C# 默认使用静态方法绑定?

32. Summarize the fundamental argument for dynamic method binding. Why do C++ and C# use static method binding by default?

33.解释 重新定义覆盖方法之间的区别。

33. Explain the distinction between redefining and overriding a method.

34.  Ada 95 中的类范围类型是什么?

34. What is a class-wide type in Ada 95?

35. 解释动态方法绑定和多态之间的联系。

35. Explain the connection between dynamic method binding and polymorphism.

36. 什么是抽象方法(在 C++ 中也称为虚方法,在 Eiffel 中也称为延迟特性)?

36. What is an abstract method (also called a pure virtual method in C++ and a deferred feature in Eiffel)?

37. 什么是反向赋值?为什么需要运行时检查?

37. What is reverse assignment? Why does it require a run-time check?

38. 什么是vtable?如何使用?

38. What is a vtable? How is it used?

39. 什么是脆弱基类问题?

39. What is the fragile base class problem?

40. 什么是抽象延迟

40. What is an abstract (deferred) class?

41. 解释虚方法对于对象闭包的重要性。

41. Explain the importance of virtual methods for object closures.

10.5 混合继承

10.5 Mix-In Inheritance

在构建面向对象系统时,设计一个完美的继承树通常很困难,其中每个类都只有一个父类。可能是动物宠物家庭成员情感对象。公司数据库中的小部件可能是可排序对象(从报告系统的角度来看)、可图形对象(从窗口系统的角度来看)或可存储对象(从文件系统的角度来看);我们如何选择一个?

When building an object-oriented system, it is often difficult to design a perfect inheritance tree, in which every class has exactly one parent. A cat may be an animal, a pet, a family_member, or an object_of_affection. A widget in the company database maybe a sortable_object (from the reporting system's perspective), a graphable_object (from the window system's perspective), or a storable_object (from the file system's perspective); how do we choose just one?

在一般情况下,我们可以想象一个类有任意数量的父类,每个父类都可以为其提供字段和方法(抽象和具体)。这种“真正的”多重继承由多种语言提供,包括 C++、Eiffel、CLOS、OCaml 和 Python;我们将在第10.6 节中讨论它。不幸的是,它在语言语义和运行时实现方面都引入了相当大的复杂性。在实践中,一种更有限的机制,称为混合继承,往往是我们真正需要的。

In the general case, we could imagine allowing a class to have an arbitrary number of parents, each of which could provide it with both fields and methods (both abstract and concrete). This sort of “true” multiple inheritance is provided by several languages, including C++, Eiffel, CLOS, OCaml, and Python; we will consider it in Section 10.6. Unfortunately, it introduces considerable complexity in both language semantics and run-time implementation. In practice, a more limited mechanism, known as mix-in inheritance, is often all we really need.

例 10.51

Example 10.51

接口的动机

The motivation for interfaces

以我们的小部件为例。很可能报告系统并没有真正定义什么小部件;它只需要能够以某些明确定义的方式操作小部件——例如,对它们进行排序。同样,窗口系统可能不需要为小部件提供任何状态或功能;它只需要能够将它们显示在屏幕上。为了满足这些要求,具有混合继承的语言允许程序员定义类必须提供的接口,以便其对象可用于特定上下文。对于小部件,报告系统可能定义一个sortable_object接口;窗口系统可能定义一个graphable_object接口;文件系统可能定义一个storable_object接口。任何接口都不会提供实际功能:小部件类的设计者需要提供适当的实现。■

Consider our widgets, for example. Odds are, the reporting system doesn't really define what a widget is; it simply needs to be able to manipulate widgets in certain well-defined ways—to sort them, for example. Likewise, the windowing system probably doesn't need to provide any state or functionality for widgets; it simply needs to be able to display them on a screen. To capture these sorts of requirements, a language with mix-in inheritance allows the programmer to define the interface that a class must provide in order for its objects to be used in certain contexts. For widgets, the reporting system might define a sortable_object interface; the window system might define a graphable_object interface; the file system might define a storable_object interface. No actual functionality would be provided by any of the interfaces: the designer of the widget class would need to provide appropriate implementations. ■

实际上,正如我们在10.4.2 节中提到的那样,接口是只包含抽象方法的类,没有字段或方法体。只要它只从一个“真实”父类继承,类就可以“混入”任意数量的接口。如果子例程的形式参数被声明为具有接口类型,那么实现(继承自)该接口的任何类都可以作为相应的实际参数传递。可以合法传递的对象类不需要具有共同的类祖先。

In effect—as we noted in Section 10.4.2—an interface is a class containing only abstract methods—no fields or method bodies. So long as it inherits from only one “real” parent, a class can “mix in” an arbitrary number of interfaces. If a formal parameter of a subroutine is declared to have an interface type, then any class that implements (inherits from) that interface can be passed as the corresponding actual parameter. The classes of objects that can legitimately be passed need not have a common class ancestor.

近年来,混合式已成为实现多重继承的常用方法(可以说是主流方法)。尽管不同语言之间的细节各不相同,但 Java、C#、Scala、Objective-C、Swift、Go、Ada 2005 和 Ruby 等语言中都出现了接口。

In recent years, mix-ins have become a common approach—arguably the dominant approach—to multiple inheritance. Though details vary from one language to another, interfaces appear in Java, C#, Scala, Objective-C, Swift, Go, Ada 2005, and Ruby, among others.

例 10.52

Example 10.52

将接口混合到派生类中

Mixing interfaces into a derived class

详细阐述我们的小部件示例,假设我们已获得通用 Java 代码,该代码将根据某个文本字段对对象进行排序,在 Web 浏览器窗口中显示对象的图形表示(根据需要隐藏和刷新),并在字典数据结构中按名称存储对对象的引用。这些功能中的每一个都将由一个接口表示。如果我们已经开发了一些复杂的widget对象类,我们可以通过将适当的接口混合到从widget派生的类中来使用通用代码,如图10.6所示。

Elaborating on our widget example, suppose that we have been given general-purpose Java code that will sort objects according to some textual field, display a graphic representation of an object within a web browser window (hiding and refreshing as appropriate), and store references to objects by name in a dictionary data structure. Each of these capabilities would be represented by an interface. If we have already developed some complicated class of widget objects, we can make use of the general-purpose code by mixing the appropriate interfaces into classes derived from widget, as shown in Figure 10.6. ■

f10-07-9780124104099
图 10.6 Java 中的接口类。通过实现named_widget中的sortable_object接口以及augmented_widget中的graphable_objectstorable_object接口,我们能够将这些类的对象传递到sorted_list.insert、browser_window.add_to_windowdictionary.insert等例程中或从这些例程中传递这些类的对象。

10.5.1 实现

10.5.1 Implementation

在 Ruby、Objective-C 或 Swift 等使用动态方法查找的语言中,接口的方法可以简单地添加到实现该接口的任何类的方法字典中。在任何需要接口类型的上下文中,通常的查找机制都会找到合适的方法。在具有完全静态类型的语言中,方法指针应该位于已知的 vtable 偏移量处,因此需要新的机制。挑战归结为需要对对象进行多种查看。

In a language like Ruby, Objective-C, or Swift, which uses dynamic method lookup, the methods of an interface can simply be added to the method dictionary of any class that implements the interface. In any context that requires the interface type, the usual lookup mechanism will find the proper methods. In a language with fully static typing, in which pointers to methods are expected to lie at known vtable offsets, new machinery is required. The challenge boils down to a need for multiple views of an object.

例 10.53

Example 10.53

混合继承的编译时实现

Compile-time implementation of mix-in inheritance

图 10.6中,方法dictionary.insert需要其参数的storable_object视图——一种查找参数的get_stored_name方法的方法。然而, get_stored_name方法是由augmented_widget实现的,并且需要其 this 参数的augmented_widget视图——一种查找对象的字段和其他方法的方法。鉴于augmented_widget实现了三个不同的接口,单个 vtable 根本无法满足要求:它的第一个条目不能同时是sortable_object、graphable_objectstorable_object的第一个方法。

In Figure 10.6, method dictionary.insert expects a storable_object view of its parameter—a way to find the parameter's get_stored_name method. The get_stored_name method, however, is implemented by augmented_widget, and will expect an augmented_widget view of its this parameter—a way to find the object's fields and other methods. Given that augmented_widget implements three different interfaces, there is no way that a single vtable can suffice: its first entry can't be the first method of sortable_object, graphable_object, and storable_object simultaneously.

最常见的解决方案(如图 10.7所示)是在每个augmented_widget对象中包含三个额外的 vtable 指针 — 每个实现的接口一个。然后,对于每个接口视图,我们可以使用指向对象相应 vtable 指针出现位置的指针。该指针与对象开头的偏移量称为“ this校正”;它存储在 vtable 的开头。

The most common solution, shown in Figure 10.7, is to include three extra vtable pointers in each augmented_widget object—one for each of the implemented interfaces. For each interface view we can then use a pointer to the place within the object where the corresponding vtable pointer appears. The offset of that pointer from the beginning of the object is known as the “this correction”; it is stored at the beginning of the vtable.

f10-08-9780124104099
图 10.7 混合继承的实现。 augmented_widget类的对象包含四个 vtable 地址,一个用于类本身(如图10.3所示),三个用于实现的接口。传递给接口例程的对象视图直接指向相关的 vtable 指针。然后,vtable 以“校正”偏移量开始,用于重新生成指向对象本身的指针。

现在假设我们希望对augmented_widget对象w调用dictionary.insert,该对象的地址当前位于寄存器r1中。编译器知道wstorable_object vtable 指针的偏移量c ,它会在将 r1 传递给insert之前将c添加到r1中。到目前为止一切顺利。当insert调用storable_object.get_stored_name 时会发生什么?假设wstorable_object视图在寄存器r1中可用,编译器将生成如下所示的代码:

Suppose now that we wish to call dictionary.insert on an augmented_widget object w, whose address is currently in register r1. The compiler, which knows the offset c of w's storable_object vtable pointer, will add c to r1 before passing it to insert. So far so good. What happens when insert calls storable_object.get_stored_name? Assuming that the storable_object view of w is available in, say, register r1, the compiler will generate code that looks something like this:

r2 := *r1–– vtable 地址
r3 := *r2–– 此修正
r3 + := r1–– w 的地址
调用*(r2+4)–– 方法地址

这里我们假设这个修正占据了 vtable 的前四个字节,并且get_stored_name的地址紧跟在它之后,在表的第一个常规槽中。我们还假设应该在寄存器r3中传递这个修正,并且没有其他参数。在典型的现代机器上,此代码比单继承所需的代码长两个指令(加载和减法)。但是,一旦执行,augmented_widget.get_stored_name将使用它所期望的参数运行:对augmented_widget对象的引用。■

Here we have assumed that the this correction occupies the first four bytes of the vtable, and that the address of get_stored_name lies immediately after it, in the table's first regular slot. We have also assumed that this should be passed in register r3, and that there are no other arguments. On a typical modern machine this code is two instructions (a load and a subtraction) longer than the code required with single inheritance. Once it executes, however, augmented_widget.get_stored_name will be running with exactly the parameter it expects: a reference to an augmented_widget object. ■

10.5.2 扩展

10.5.2 Extensions

上述接口描述反映了 Java 的历史版本,但有一点被忽略:除了抽象方法之外,接口还可以定义静态 final(常量)字段。由于此类字段永远不会改变,因此不会引入运行时复杂性或开销 — 编译器可以在使用它们的任何地方有效地就地扩展它们。

The description of interfaces above reflects historical versions of Java, with one omission: in addition to abstract methods, an interface can define static final (constant) fields. Because such fields can never change, they introduce no runtime complexity or overhead—the compiler can, effectively, expand them in place wherever they are used.

从 Java 8 开始,接口也得到了扩展,允许使用静态方法默认方法,这两种方法在接口声明中都有主体(代码)。静态方法(如静态 final字段)不会引入实现复杂性:它不需要访问对象字段,因此不会产生将哪个视图作为this传递的歧义—没有this参数。默认方法有点棘手。它们的代码旨在供任何未覆盖它的类使用。这种约定对于库来说尤其有价值维护者:它允许将新方法添加到现有的库接口中,而不会破坏现有的用户代码,否则必须更新现有用户代码才能在从该接口继承的任何类中实现新方法。

Beginning with Java 8, interfaces have also been extended to allow static and default methods, both of which are given bodies—code—in the declaration of the interface. A static method, like a static final field, introduces no implementation complexity: it requires no access to object fields, so there is no ambiguity about what view to pass as this—there is no this parameter. Default methods are a bit more tricky. Their code is intended to be used by any class that does not override it. This convention is particularly valuable for library maintainers: it allows new methods to be added to an existing library interface without breaking existing user code, which would otherwise have to be updated to implement the new methods in any class that inherits from the interface.

例 10.54

Example 10.54

使用默认方法

Use of default methods

例如,假设我们正在从事一个本地化项目,旨在使一些现有代码适应多种语言和文化。在图 10.6的代码中,我们可能希望向storable_object接口添加一个新的get_local_name方法。给定对storable_object的引用,更新后的用户代码可以调用这个新方法,而不是get_stored_name,来获取适合在本地上下文中使用的字符串。从storable_object继承并已作为本地化项目的一部分进行更新的具体类可能会提供它自己的get_local_name实现。但那些尚未更新(或可能永远不会更新)的类呢?这些可以利用默认方法来依赖某种通用的翻译机制:

Suppose, for example, that we are engaged in a localization project, which aims to adapt some existing code to multiple languages and cultures. In the code of Figure 10.6, we might wish to add a new get_local_name method to the storable_object interface. Given a reference to a storable_object, updated user code could then call this new method, rather than get_stored_name, to obtain a string appropriate for use in the local context. A concrete class that inherits from storable_object, and that has been updated as part of the localization project, might provide its own implementation of get_local_name. But what about classes that haven't been updated yet (or that may never be updated)? These could leverage default methods to fall back on some general-purpose translation mechanism:

默认字符串 get_local_name() {

default String get_local_name() {

 返回备份翻译(get_stored_name());

 return backup_translation(get_stored_name());

}

}

要使用默认值,每个从storable_object继承的具体类都需要重新编译,但其源代码可以保持不变。■

To use the default, each concrete class that inherits from storable_object would need to be recompiled, but its source code could remain unchanged. ■

例 10.55

Example 10.55

默认方法的实现

Implementation of default methods

由于默认方法是在接口声明中定义的,因此它只能看到接口本身的方法和(静态)字段(以及周围范围的任何可见名称)。特别是,它无法访问从接口继承的类的其他成员,因此不需要允许它找到这些成员的对象视图。同时,该方法确实需要访问对象特定于接口的 vtable。在我们的storable_object示例中,默认的get_local_name必须能够找到并调用由具体类定义的get_stored_name版本。实现此访问的通常方法取决于微小的转发例程:对于从storable_object继承并需要默认代码的每个类C ,编译器都会生成一个静态的、特定于C的转发例程,该例程接受特定于具体类的this参数,将常规调用序列刚刚减去的this校正添加回去,并将生成的指向 vtable 指针的指针传递给默认方法。■

Because a default method is defined within the interface declaration, it can see only the methods and (static) fields of the interface itself (along with any visible names from surrounding scopes). In particular, it has no access to other members of classes that inherit from the interface, and thus no need of an object view that would allow it to find those members. At the same time, the method does require access to the object's interface-specific vtable. In our storable_object example, the default get_local_name has to be able to find, and call, the version of get_stored_name defined by the concrete class. The usual way to implement this access depends on tiny forwarding routines: for each class C that inherits from storable_object and that needs the default code, the compiler generates a static, C-specific forwarding routine that accepts the concrete-class-specific this parameter, adds back in the this correction that the regular calling sequence just subtracted out, and passes the resulting pointer-to-vtable-pointer to the default method. ■

事实证明,Scala 编程语言早已提供了与默认方法等效的功能,其混合函数称为特征。实际上,特征不仅支持默认方法,还支持可变字段。Scala 编译器不会尝试创建一个视图来使这些字段可直接访问,而是为每个继承自特征的具体类生成一对隐藏的访问器方法,类似于C# 的属性示例 10.7)。然后,对访问器方法的引用将包含在接口特定的 vtable 中,在那里它们可以通过默认方法调用。在任何未提供自己的特征字段定义的类中,编译器都会创建一个新的私有字段供访问器方法使用。

As it turns out, the equivalent of default methods has long been provided by the Scala programming language, whose mix-ins are known as traits. In fact, traits support not only default methods but also mutable fields. Rather than try to create a view that would make these fields directly accessible, the Scala compiler generates, for each concrete class that inherits from the trait, a pair of hidden accessor methods analogous to the properties of C# (Example 10.7). References to the accessor methods are then included in the interface-specific vtable, where they can be called by default methods. In any class that does not provide its own definition of a trait field, the compiler creates a new private field to be used by the accessor methods.

10.6 真正的多重继承

10.6 True Multiple Inheritance

如第 10.5 节所述,混合继承允许接口指定继承类必须提供的功能,以便该类的对象可以在给定上下文中使用。至关重要的是,接口在大多数情况下并不提供该功能本身。即使是默认方法也主要用于协调对继承类提供的功能的访问。

As described in Section 10.5, mix-in inheritance allows an interface to specify functionality that must be provided by an inheriting class in order for objects of that class to be used in a given context. Crucially, an interface does not, for the most part, provide that functionality itself. Even default methods serve mainly to orchestrate access to functionality provided by the inheriting class.

例 10.56

Example 10.56

从两个基类派生

Deriving from two base classes

有时从多个基类继承实际功能会很有用。例如,假设我们的管理计算系统需要跟踪每个系统用户的信息,并且大学为每个学生提供一个帐户。那么可能需要从personsystem_user派生类student。在 C++ 中,我们可以说

At times it can be useful to inherit real functionality from more than one base class. Suppose, for example, that our administrative computing system needs to keep track of information about every system user, and that the university provides every student with an account. It may then be desirable to derive class student from both person and system_user. In C++ we can say

学生班级:公众人物,公共系统用户{…

class student : public person, public system_user { …

现在student类的对象将拥有personsystem_user的所有字段和方法。Eiffel 中的声明类似:

Now an object of class student will have all the fields and methods of both a person and a system_user. The declaration in Eiffel is analogous:

班级学生

class student

继承

inherit

 

 person

 系统用户

 system_user

特征

feature

 … ■

 … ■

真正的多重继承也出现在其他几种语言中,包括 CLOS、OCaml 和 Python。许多较老的语言,包括 Simula、Smalltalk、Modula-3 和 Oberon,只提供单一继承。混合继承是一种常见的折衷方案。

True multiple inheritance appears in several other languages as well, including CLOS, OCaml, and Python. Many older languages, including Simula, Smalltalk, Modula-3, and Oberon, provided only single inheritance. Mix-in inheritance is a common compromise.

10-02-9780124104099 更深入地

IN MORE DEPTH

多重继承引入了大量的语义和实际问题,我们在配套站点上对此进行了考虑:

Multiple inheritance introduces a wealth of semantic and pragmatic issues, which we consider on the companion site:

 假设两个父类提供了同名的方法。我们在子类中使用哪一个?我们可以同时访问这两个方法吗?

 Suppose two parent classes provide a method with the same name. Which one do we use in the child? Can we access both?

 假设两个父类都派生自某个共同的“祖父”类。“孙子”类是否有祖父类字段的一个副本或两个副本?

 Suppose two parent classes are both derived from some common “grandparent” class. Does the “grandchild” have one copy or two of the grandparent's fields?

 我们实现单继承依赖于这样一个事实:父类对象的表示是派生类对象的表示的前缀。在多重继承中,每个父类如何成为子类的前缀?

 Our implementation of single inheritance relies on the fact that the representation of an object of the parent class is a prefix of the representation of an object of a derived class. With multiple inheritance, how can each parent be a prefix of the child?

具有共同“祖父母”的多重继承称为重复继承。具有祖父母单独副本的重复继承称为复制继承;具有祖父母单个副本的重复继承称为共享继承。共享继承是 Eiffel 中的默认设置。复制继承是 C++ 中的默认设置。这两种语言都允许程序员在需要时获得另一个选项。

Multiple inheritance with a common “grandparent” is known as repeated inheritance. Repeated inheritance with separate copies of the grandparent is known as replicated inheritance; repeated inheritance with a single copy of the grandparent is known as shared inheritance. Shared inheritance is the default in Eiffel. Replicated inheritance is the default in C++. Both languages allow the programmer to obtain the other option when desired.

10.7 重新审视面向对象编程

10.7 Object-Oriented Programming Revisited

在本章开头,我们用三个基本概念来描述面向对象编程:封装、继承和动态方法绑定。封装允许将抽象的实现细节隐藏在简单的接口后面。继承允许将新抽象定义为某些现有抽象的扩展或细化,从而自动获得其部分或全部特性。动态方法绑定允许新抽象即使在需要旧抽象的上下文中使用时也能显示其新行为。

At the beginning of this chapter, we characterized object-oriented programming in terms of three fundamental concepts: encapsulation, inheritance, and dynamic method binding. Encapsulation allows the implementation details of an abstraction to be hidden behind a simple interface. Inheritance allows a new abstraction to be defined as an extension or refinement of some existing abstraction, obtaining some or all of its characteristics automatically. Dynamic method binding allows the new abstraction to display its new behavior even when used in a context that expects the old abstraction.

不同的编程语言对这些基本概念的支持程度不同。具体来说,语言在要求程序员以面向对象风格编写的程度上有所不同。一些作者认为,真正的面向对象语言应该使编写非面向对象的程序变得困难或不可能。从这个纯粹主义的观点来看,面向对象语言应该呈现统一的计算对象模型,其中每种数据类型都是一个类,每个变量都是对对象的引用,每个子例程都是一个对象方法。此外,应该以拟人化的方式考虑对象:作为负责所有计算的活动实体。

Different programming languages support these fundamental concepts to different degrees. In particular, languages differ in the extent to which they require the programmer to write in an object-oriented style. Some authors argue that a truly object-oriented language should make it difficult or impossible to write programs that are not object-oriented. From this purist point of view, an object-oriented language should present a uniform object model of computing, in which every data type is a class, every variable is a reference to an object, and every subroutine is an object method. Moreover, objects should be thought of in anthropomorphic terms: as active entities responsible for all computation.

Smalltalk 和 Ruby 接近这一理想。事实上,正如下面小节(主要在配套网站上)所述,在 Smalltalk 中,甚至诸如选择和迭代之类的控制流机制也被建模为方法调用。另一方面,Ada 95 和 Fortran 2003 可能最好被描述为冯·诺依曼语言,允许程序员在需要时以面向对象风格编写代码。

Smalltalk and Ruby come close to this ideal. In fact, as described in the subsection below (mostly on the companion site), even such control-flow mechanisms as selection and iteration are modeled as method invocations in Smalltalk. On the other hand, Ada 95 and Fortran 2003 are probably best characterized as von Neumann languages that permit the programmer to write in an object-oriented style if desired.

那么 C++ 又如何呢?它确实具有丰富的特性,包括面向对象程序中很有用的、Smalltalk 所没有的几个特性(多重继承、复杂的访问控制、严格的初始化顺序、析构函数、泛型)。与此同时,它还存在许多问题。它的简单类型不是类。它在类之外有子例程。它默认使用静态方法绑定和复制多重继承,而不是成本更高的虚拟替代方案。它的未经检查的 C 风格类型转换为类型检查和访问控制提供了重大漏洞。它缺乏垃圾收集,这是创建正确、自足的抽象的主要障碍。可能最严重的是,C++ 保留了 C 的所有低级机制,允许程序员逃避或颠覆面向对象的编程模型完全不同。有人认为,最好的 C++ 程序员是那些没有学习 C 语言的人:他们不太想用新语言编写“C 风格”的程序。总的来说,可以说 C++ 是一种面向对象的语言,就像 Common Lisp 是一种函数式语言一样。除了垃圾收集之外,C++ 提供了所有必要的工具,但程序员需要大量的纪律才能“正确”使用这些工具。

So what about C++? It certainly has a wealth of features, including several (multiple inheritance, elaborate access control, strict initialization order, destructors, generics) that are useful in object-oriented programs and that are not found in Smalltalk. At the same time, it has a wealth of problematic wrinkles. Its simple types are not classes. It has subroutines outside of classes. It uses static method binding and replicated multiple inheritance by default, rather than the more costly virtual alternatives. Its unchecked C-style type casts provide a major loophole for type checking and access control. Its lack of garbage collection is a major obstacle to the creation of correct, self-contained abstractions. Probably most serious of all, C++ retains all of the low-level mechanisms of C, allowing the programmer to escape or subvert the object-oriented model of programming entirely. It has been suggested that the best C++ programmers are those who did not learn C first: they are not as tempted to write “C-style” programs in the newer language. On balance, it is probably safe to say that C++ is an object-oriented language in the same sense that Common Lisp is a functional language. With the possible exception of garbage collection, C++ provides all of the necessary tools, but it requires substantial discipline on the part of the programmer to use those tools “correctly.”

10.7.1 Smalltalk 的对象模型

10.7.1 The Object Model of Smalltalk

从历史上看,Smalltalk 被认为是面向对象语言的典范。该语言的原始版本由 Alan Kay 在 20 世纪 60 年代末作为犹他大学博士工作的一部分设计。它随后被施乐帕洛阿尔托研究中心 (PARC) 的软件概念小组采用,并在 20 世纪 70 年代经历了五次重大修订,最终形成了 Smalltalk-80 语言。7

Historically, Smalltalk was considered the canonical object-oriented language. The original version of the language was designed by Alan Kay as part of his doctoral work at the University of Utah in the late 1960s. It was then adopted by the Software Concepts Group at the Xerox Palo Alto Research Center (PARC), and went through five major revisions in the 1970s, culminating in the Smalltalk-80 language.7

10-02-9780124104099 更深入地

IN MORE DEPTH

我们在前面的章节中提到了 Smalltalk 的几个特性。在配套网站上可以找到更长的介绍,我们特别关注 Smalltalk 的拟人化编程模型。对该语言的完整介绍超出了本书的范围。

We have mentioned several features of Smalltalk in previous sections. A somewhat longer treatment can be found on the companion site, where we focus in particular on Smalltalk's anthropomorphic programming model. A full introduction to the language is beyond the scope of this book.

10-01-9780124104099检查你的理解

Check Your Understanding

42. 什么是混合继承?它解决了什么问题?

42. What is mix-in inheritance? What problem does it solve?

43.概述静态类型对象语言中混合继承的可能实现。特别说明 对象对接口特定视图的需求。

43. Outline a possible implementation of mix-in inheritance for a language with statically typed objects. Explain in particular the need for interface-specific views of an object.

44. 描述如何使用默认方法实现、静态(常量)字段甚至可变字段来扩展混合(及其实现)。

44. Describe how mix-ins (and their implementation) can be extended with default method implementations, static (constant) fields, and even mutable fields.

45. 真正的多重继承可以实现什么,而混合继承却不能?

45. What does true multiple inheritance make possible that mix-in inheritance does not?

46. 什么是重复继承?复制重复继承和共享重复继承的区别是什么?

46. What is repeated inheritance? What is the distinction between replicated and shared repeated inheritance?

47. 对于一种语言来说,提供统一的对象模型意味着什么?请说出两种提供统一对象模型的语言。

47. What does it mean for a language to provide a uniform object model?. Name two languages that do so.

10.8 总结和结束语

10.8 Summary and Concluding Remarks

这是我们关于语言设计的六个核心章节中的最后一章:名称(第 3 章)、控制流(第 6 章)、类型系统(第 7 章)、复合类型(第 8 章)、子程序(第 9 章)和对象(第 10 章)。

This has been the last of our six core chapters on language design: names (Chapter 3), control flow (Chapter 6), type systems (Chapter 7), composite types (Chapter 8), subroutines (Chapter 9), and objects (Chapter 10).

我们从10.1 节开始,介绍了面向对象编程的三个基本概念:封装、继承动态方法绑定。我们还介绍了类、对象和方法的术语。我们已经在第 3 章的模块中看到了封装。封装允许将复杂数据抽象的细节隐藏在相对简单的接口后面。继承扩展了封装的实用性,使程序员可以轻松地将新的抽象定义为现有抽象的改进或扩展。继承为多态子程序提供了自然的基础:如果子程序期望给定类的实例作为参数,那么可以使用从预期类派生的任何类的对象(假设它保留了整个现有接口)。动态方法绑定通过安排对参数方法之一的调用在运行时使用与实际对象的类相关联的实现,而不是与参数的声明类相关联的实现,扩展了这种形式的多态性。我们注意到,一些语言,包括 Modula-3、Oberon、Ada 95 和 Fortran 2003,通过类型扩展机制支持面向对象,其中封装与模块相关,但继承和动态方法绑定与特殊形式的记录相关。

We began in Section 10.1 by identifying three fundamental concepts of object-oriented programming: encapsulation, inheritance, and dynamic method binding. We also introduced the terminology of classes, objects, and methods. We had already seen encapsulation in the modules of Chapter 3. Encapsulation allows the details of a complicated data abstraction to be hidden behind a comparatively simple interface. Inheritance extends the utility of encapsulation by making it easy for programmers to define new abstractions as refinements or extensions of existing abstractions. Inheritance provides a natural basis for polymorphic subroutines: if a subroutine expects an instance of a given class as argument, then an object of any class derived from the expected one can be used instead (assuming that it retains the entire existing interface). Dynamic method binding extends this form of polymorphism by arranging for a call to one of the parameter's methods to use the implementation associated with the class of the actual object at run time, rather than the implementation associated with the declared class of the parameter. We noted that some languages, including Modula-3, Oberon, Ada 95, and Fortran 2003, support object orientation through a type extension mechanism, in which encapsulation is associated with modules, but inheritance and dynamic method binding are associated with a special form of record.

在后面的章节中,我们详细介绍了对象初始化和终止、动态方法绑定和(在配套站点上)多重继承。在许多情况下,我们发现功能性和简单性与执行速度之间存在权衡。将变量视为引用而不是值通常会产生更简单的语义,但需要额外的间接性。如前文第8.5.3 节所述,垃圾收集大大简化了软件的创建和维护,但会产生运行时成本。动态方法绑定要求(在一般情况下)使用 vtable 或其他查找机制来调度方法。多重继承的完全通用实现往往会在未使用时产生开销。

In later sections we covered object initialization and finalization, dynamic method binding, and (on the companion site) multiple inheritance in some detail. In many cases we discovered tradeoffs between functionality on the one hand and simplicity and execution speed on the other. Treating variables as references, rather than values, often leads to simpler semantics, but requires extra indirection. Garbage collection, as previously noted in Section 8.5.3, dramatically eases the creation and maintenance of software, but imposes run-time costs. Dynamic method binding requires (in the general case) that methods be dispatched using vtables or some other lookup mechanism. Fully general implementations of multiple inheritance tend to impose overheads even when unused.

在一些情况下,我们还看到了时间/空间的权衡。如前文第 9.2.4 节所述,内联子程序可以显著提高包含许多小子程序的代码的性能,这不仅可以消除子程序调用本身的开销,还可以允许寄存器分配、公共子表达式分析和其他“全局”代码改进将应用于调用。同时,内联扩展通常会增加目标代码的大小。练习 C-10.28C-10.30探讨了多重继承实现中的类似权衡。

In several cases we saw time/space tradeoffs as well. In-line subroutines, as previously noted in Section 9.2.4, can dramatically improve the performance of code with many small subroutines, not only by eliminating the overhead of the subroutine calls themselves, but by allowing register allocation, common subexpression analysis, and other “global” code improvements to be applied across calls. At the same time, in-line expansion generally increases the size of object code. Exercises C-10.28 and C-10.30 explore similar tradeoffs in the implementation of multiple inheritance.

从历史上看,Smalltalk 被广泛认为是最纯粹、最灵活的面向对象语言。然而,它缺乏编译时类型检查,加上其“基于消息”的计算模型以及对动态方法查找的需求,这往往使其实现速度相当慢。C++ 具有对象值变量、默认静态绑定、最少的动态检查和高质量的编译器,在很大程度上推动了 20 世纪 90 年代面向对象编程的普及。如今,对象无处不在 - 在静态类型、编译型语言(如 Java 和 C#)中;在动态类型语言(如 Python、Ruby、PHP 和 JavaScript)中;甚至在基于二进制组件或万维网上人类可读的服务调用的系统中(有关这些内容的更多信息,请参阅书目注释)。

Historically, Smalltalk was widely regarded as the purest and most flexible of the object-oriented languages. Its lack of compile-time type checking, however, together with its “message-based” model of computation and its need for dynamic method lookup, tended to make its implementations rather slow. C++, with its object-valued variables, default static binding, minimal dynamic checks, and high-quality compilers, was largely responsible for popularizing object-oriented programming in the 1990s. Today objects are ubiquitous—in statically typed, compiled languages like Java and C#; in dynamically typed languages like Python, Ruby, PHP, and JavaScript; and even in systems based on binary components or human-readable service invocations over the World Wide Web (more on these in the Bibliographic Notes).

10.9 练习

10.9 Exercises

10.1 一些语言设计者认为面向对象消除了对嵌套子程序的需求。你同意吗?为什么或为什么不?

10.1 Some language designers argue that object orientation eliminates the need for nested subroutines. Do you agree? Why or why not?

10.2设计一个类层次结构来表示 图 4.5中 CFG 的语法树。在每个类中提供一个方法来返回节点的值。提供充当make_leaf、make_un_opmake_bin_op子例程角色的构造函数。

10.2 Design a class hierarchy to represent syntax trees for the CFG of Figure 4.5. Provide a method in each class to return the value of a node. Provide constructors that play the role of the make_leaf, make_un_op, and make_bin_op subroutines.

10.3 重复上一个练习,但使用变体记录(联合)类型来表示语法树节点。使用类型扩展再次重复。从清晰度、抽象性、类型安全性和可扩展性方面比较这三种解决方案。

10.3 Repeat the previous exercise, but using a variant record (union) type to represent syntax tree nodes. Repeat again using type extensions. Compare the three solutions in terms of clarity, abstraction, type safety, and extensibility.

10.4 使用 C#索引器机制,创建一个可以像数组一样进行索引的哈希表类。(实际上,创建System.Collections.Hashtable容器类的简单版本。)或者,使用运算符 []的重载版本在 C++ 中构建类似的类。

10.4 Using the C# indexer mechanism, create a hash table class that can be indexed like an array. (In effect, create a simple version of the System.Collections.Hashtable container class.) Alternatively, use an overloaded version of operator[] to build a similar class in C++.

10.5本着 示例 10.8的精神,编写一个双端队列(deque)抽象(发音为“deck”),源自双向链表基类。

10.5 In the spirit of Example 10.8, write a double-ended queue (deque) abstraction (pronounced “deck”), derived from a doubly linked list base class.

10.6 使用模板(泛型)根据容器中的数据类型抽象出前两个问题的解决方案。

10.6 Use templates (generics) to abstract your solutions to the previous two questions over the type of data in the container.

10.7用 Python 或 Ruby 重复练习 10.5。编写一个简单的程序来演示泛型不需要抽象类型。如果在同一个双端队列中混合不同类型的对象会发生什么?

10.7 Repeat Exercise 10.5 in Python or Ruby. Write a simple program to demonstrate that generics are not needed to abstract over types. What happens if you mix objects of different types in the same deque?

10.8 使用示例 10.17中的列表类时,典型的 C++ 程序员将使用指针类型作为泛型参数V,以便list_nodes指向列表的元素。另一种实现方式是包含nextprev 在元素本身中为列表添加指针——通常是通过安排元素类型从类似示例 10.14中的gp_list_node类继承。结果有时被称为侵入式列表。

10.8 When using the list class of Example 10.17, the typical C++ programmer will use a pointer type for generic parameter V, so that list_nodes point to the elements of the list. An alternative implementation would include next and prev pointers for the list within the elements themselves—typically by arranging for the element type to inherit from something like the gp_list_node class of Example 10.14. The result is sometimes called an intrusive list.

(a) 解释如何在 C++ 中构建侵入式列表,而无需用户在其代码中加入显式类型转换。提示:给定多重继承,您可能需要确定每个具体元素类型在类型表示中 nextprev指针出现的偏移量。有关更多想法,请搜索有关流行 Boost 库的boost::intrusive::list类的信息。

(a) Explain how you might build intrusive lists in C++ without requiring users to pepper their code with explicit type casts. Hint: given multiple inheritance, you will probably need to determine, for each concrete element type, the offset within the representation of the type at which the next and prev pointers appear. For further ideas, search for information on the boost::intrusive::list class of the popular Boost library.

(b) 讨论侵入式和非侵入式列表的相对优点和缺点。

(b) Discuss the relative advantages and disadvantages of intrusive and non-intrusive lists.

10.9你能用 C# 或 C++ 模拟 示例 10.22的内部类吗?(提示:你需要 Java 对周围类的隐藏引用的显式版本。)

10.9 Can you emulate the inner class of Example 10.22 in C# or C++? (Hint: You'll need an explicit version of Java's hidden reference to the surrounding class.)

10.10为 图 10.2的列表抽象编写一个包主体。

10.10 Write a package body for the list abstraction of Figure 10.2.

10.11 用 Eiffel、Java 和/或 C# 重写列表和队列抽象。

10.11 Rewrite the list and queue abstractions in Eiffel, Java, and/or C#.

10.12 使用 C++、Java 或 C#,按照示例 10.25的精神实现一个Complex类。讨论在对象状态中保留所有四个值(xyρθ)与仅保留两个值并根据需要计算其他值之间的时间和空间权衡。

10.12 Using C++, Java, or C#, implement a Complex class in the spirit of Example 10.25. Discuss the time and space tradeoffs between maintaining all four values (x, y, ρ, and θ) in the state of the object, or keeping only two and computing the others on demand.

10.13 对 Python 和/或 Ruby 重复前两个练习。

10.13 Repeat the previous two exercises for Python and/or Ruby.

10.14 比较 Java最终方法与 C++ 非虚拟方法。它们有何相同之处?有何不同之处?

10.14 Compare Java final methods with C++ nonvirtual methods. How are they the same? How are they different?

10.15 在一些面向对象语言中,包括 C++ 和 Eiffel,派生类可以隐藏基类的成员。例如,在 C++ 中,我们可以将基类声明为public、protectedprivateclass B:public A { … // A 的公共成员是 B 的公共成员// A 的受保护成员是 B 的受保护成员class C:protected A { … // A 的公共和受保护成员是 C 的受保护成员class D:private A { … // A 的公共和受保护成员是 D 的私有成员在所有情况下,BCD的方法都无法访问A私有成员。考虑受保护私有基类对动态方法绑定的影响。在什么情况下可以将对类BCD的对象的引用赋给类型A*的变量?

 

  

  

 

 

  

 

 

  



10.15 In several object-oriented languages, including C++ and Eiffel, a derived class can hide members of the base class. In C++, for example, we can declare a base class to be public, protected, or private:

 class B : public A { …

  // public members of A are public members of B

  // protected members of A are protected members of B

 

 class C : protected A { …

  // public and protected members of A are protected members of C

 

 class D : private A { …

  // public and protected members of A are private members of D

In all cases, private members of A are inaccessible to methods of B, C, or D.

Consider the impact of protected and private base classes on dynamic method binding. Under what circumstances can a reference to an object of class B, C, or D be assigned into a variable of type A*?

10.16 如果我们重新定义数据成员,类的实现会发生什么?例如,假设我们有class foo { public: int a; char *b; }; class bar : public foo { public: float c; int b; }; bar对象的表示包含一个b字段还是两个?如果是两个,是否可以访问,还是只能访问一个?在什么情况下?

 

 

  

  

 

 

 

 

  

  

 

10.16 What happens to the implementation of a class if we redefine a data member? For example, suppose we have

 class foo {

 public:

  int a;

  char *b;

 };

 

 class bar : public foo {

 public:

  float c;

  int b;

 };

Does the representation of a bar object contain one b field or two? If two, are both accessible, or only one? Under what circumstances?

10.17 讨论类和类型扩展的相对优点。你更喜欢哪一个?为什么?

10.17 Discuss the relative merits of classes and type extensions. Which do you prefer? Why?

10.18根据 示例 10.28的提纲,编写一个程序来说明 C++ 中复制构造函数和运算符=之间的区别。您的代码应包括可能调用这些函数的每种情况的示例(不要忘记参数传递和函数返回)。在每个类中检测复制构造函数和赋值运算符,以便它们在调用时打印其名称。运行您的程序以验证其行为是否符合您的预期。

10.18 Building on the outline of Example 10.28, write a program that illustrates the difference between copy constructors and operator= in C++. Your code should include examples of each situation in which one of these may be called (don't forget parameter passing and function returns). Instrument the copy constructors and assignment operators in each of your classes so that they will print their names when called. Run your program to verify that its behavior matches your expectations.

10.19 您如何看待 C++、C# 和 Ada 95 中默认使用静态方法绑定而不是动态方法绑定的决定?实现速度的提高是否值得以抽象和可重用性的损失为代价?假设我们有时需要静态绑定,您更喜欢 C++ 和 C# 的逐个方法方法,还是 Ada 95 的逐个变量方法?为什么?

10.19 What do you think of the decision, in C++, C#, and Ada 95, to use static method binding, rather than dynamic, by default? Is the gain in implementation speed worth the loss in abstraction and reusability? Assuming that we sometimes want static binding, do you prefer the method-by-method approach of C++ and C#, or the variable-by-variable approach of Ada 95? Why?

10.20 如果foo是 C++ 程序中的抽象类,为什么可以声明foo*类型的变量,但不能声明foo类型的变量?

10.20 If foo is an abstract class in a C++ program, why is it acceptable to declare variables of type foo*, but not of type foo?

10.21考虑 图 10.8所示的 Java 程序。假设该程序将在具有 4 字节地址的机器上编译为本机代码。

10.21 Consider the Java program shown in Figure 10.8. Assume that this is to be compiled to native code on a machine with 4-byte addresses.

f10-09-9780124104099
图 10.8 Java 中的一个简单程序。

(a) 画出第15行创建的对象在内存中的布局。显示所有虚函数表。

(a) Draw a picture of the layout in memory of the object created at line 15. Show all virtual function tables.

(二) 给出第 19 行调用c.val的汇编级伪代码。您可以假设c的地址在调用之前立即位于寄存器r1中,并且应该使用同一寄存器来传递隐藏的this参数。您可以忽略保存和恢复寄存器的需要,而不必担心将返回值放在哪里。

(b) Give assembly-level pseudocode for the call to c.val at line 19. You may assume that the address of c is in register r1 immediately before the call, and that this same register should be used to pass the hidden this parameter. You may ignore the need to save and restore registers, and don't worry about where to put the return value.

(c)给出第 17 行对 c.ping的调用的汇编级伪代码。同样假设c的地址在寄存器r1中,这是用于传递this 的同一寄存器,并且您不需要保存或恢复任何寄存器。

(c) Give assembly-level pseudocode for the call to c.ping at line 17. Again, assume that the address of c is in register r1, that this is the same register that should be used to pass this, and that you don't need to save or restore any registers.

(d)给出方法 Counter.ping主体的汇编级伪代码(再次忽略寄存器保存/恢复)。

(d) Give assembly-level pseudocode for the body of method Counter.ping (again ignoring register save/restore).

10.22 在 Ruby 中,与 Java 8 或 Scala 一样,接口(混合)可以提供方法代码以及签名。(它不能提供数据成员;否则会造成多重继承。)解释为什么动态类型使此功能比其他语言更强大。

10.22 In Ruby, as in Java 8 or Scala, an interface (mix-in) can provide method code as well as signatures. (It can't provide data members; that would be multiple inheritance.) Explain why dynamic typing makes this feature more powerful than it is in the other languages.

10-02-9780124104099 10.23–10.31 更深入。

10.23–10.31  In More Depth.

10.10 探索

10.10 Explorations

10.32 回到练习3.7 。构建图 3.16中单链表库的(更完整的)C++ 版本。讨论存储管理问题。在什么情况下删除列表本身时应该删除列表的元素?list_node的析构函数应该做什么?它应该删除其数据成员吗?它应该递归删除下一个节点吗?

10.32 Return for a moment to Exercise 3.7. Build a (more complete) C++ version of the singly linked list library of Figure 3.16. Discuss the issue of storage management. Under what circumstances should one delete the elements of a list when deleting the list itself? What should the destructor for list_node do? Should it delete its data member? Should it recursively delete node next?

10.33 本章的讨论集中在面向对象编程语言的经典“基于类”方法上,该方法由 Simula 和 Smalltalk 开创。还有一种替代方法,即“基于对象”方法,它摒弃了类的概念。在基于对象的编程中,方法直接与对象相关联,并使用现有对象作为原型来创建新对象。了解 Self(标准的基于对象的编程语言)和 JavaScript(使用最广泛的编程语言)。你如何看待它们的方法?它与基于类的替代方案相比如何?阅读第14.4.4 节中有关 JavaScript 的内容可能会对你有所帮助。

10.33 The discussion in this chapter has focused on the classic “class-based” approach to object-oriented programming languages, pioneered by Simula and Smalltalk. There is an alternative, “object-based” approach that dispenses with the notion of class. In object-based programming, methods are directly associated with objects, and new objects are created using existing objects as prototypes. Learn about Self, the canonical object-based programming language, and JavaScript, the most widely used. What do you think of their approach? How does it compare to the class-based alternative? You may find it helpful to read the coverage of JavaScript in Section 14.4.4.

10.34 如 C-5.5.1 节所述,流水线处理器的性能主要取决于硬件成功预测分支结果的能力,以便后续指令的处理可以在分支处理完成之前开始。然而,在面向对象程序中,仅仅知道分支结果是不够的:因为分支通常通过 vtable 进行分派,所以还必须预测目标了解一个或多个现代处理器中分支预测的工作原理。这些处理器处理面向对象程序的效果如何?

10.34 As described in Section C-5.5.1, performance on pipelined processors depends critically on the ability of the hardware to successfully predict the outcome of branches, so that processing of subsequent instructions can begin before processing of the branch has completed. In object-oriented programs, however, knowing the outcome of a branch is not enough: because branches are so often dispatched through vtables, one must also predict the destination. Learn how branch prediction works in one or more modern processors. How well do these processors handle object-oriented programs?

10.35 探索即时(本机代码)Java 编译器中混合继承的实现。它是否遵循第 10.5 节的策略?它的效率如何?

10.35 Explore the implementation of mix-in inheritance in a just-in-time (native code) Java compiler. Does it follow the strategy of Section 10.5? How efficient is it?

10.36 探究 Ruby 中混合继承的实现。它与 Java 的实现有何不同?

10.36 Explore the implementation of mix-in inheritance in Ruby. How does it differ from that of Java?

10.37 了解类型层次分析类型传播,它们有时可用于在编译时推断对象的具体类型,从而允许编译器生成对方法的直接调用,而不是通过 vtable 间接调用。这些技术有多有效?在典型的基准测试中,它们能够优化多少方法调用?它们的局限性是什么?(您可以从 Bacon 和 Sweeney [ BS96 ] 和 Diwan 等人 [ DMM96 ] 的论文开始。)

10.37 Learn about type hierarchy analysis and type propagation, which can sometimes be used to infer the concrete type of objects at compile time, allowing the compiler to generate direct calls to methods, rather than indirecting through vtables. How effective are these techniques? What fraction of method calls are they able to optimize in typical benchmarks? What are their limitations? (You might start with the papers of Bacon and Sweeney [BS96] and Diwan et al. [DMM96].)

10-02-9780124104099 10.38–10.39 更深入。

10.38–10.39  In More Depth.

10.11 书目注释

10.11 Bibliographic Notes

附录 A包含本章讨论的各种语言的书目引用,包括 Simula、Smalltalk、C++、Eiffel、Java、C#、Modula-3、Oberon、Ada 95、Fortran 2003、Python、Ruby、Objective-C、Swift、Go、OCaml、和 CLOS。Lisp 的其他面向对象版本包括 Loops [ BS83 ] 和 Flavors [ Moo86 ]。

Appendix A contains bibliographic citations for the various languages discussed in this chapter, including Simula, Smalltalk, C++, Eiffel, Java, C#, Modula-3, Oberon, Ada 95, Fortran 2003, Python, Ruby, Objective-C, Swift, Go, OCaml, and CLOS. Other object-oriented versions of Lisp include Loops [BS83] and Flavors [Moo86].

Ellis 和 Stroustrup [ ES90 ] 对 C++ 历史版本的语义和实用问题进行了广泛的讨论。Stroustrup 文本 [ Str13 ] 的第三和第四部分全面概述了 C++ 中容器类的设计和实现。Deutsch 和 Schiffman [ DS84 ] 介绍了高效实现 Smalltalk 的技术。Borning 和 Ingalls [ BI82 ] 讨论了 Smalltalk-80 扩展中的多重继承。Strongtalk [ Sun06 ] 是 Sun Microsystems 于 20 世纪 90 年代开发的 Smalltalk 强类型后继者,后来作为开源发布。Gil 和 Sweeney [ GS99 ] 介绍了可用于降低多重继承的时间和空间复杂度的优化。

Ellis and Stroustrup [ES90] provide extensive discussion of both semantic and pragmatic issues for historic versions of C++. Parts III and IV of Stroustrup's text [Str13] provide a comprehensive survey of the design and implementation of container classes in C++. Deutsch and Schiffman [DS84] describe techniques to implement Smalltalk efficiently. Borning and Ingalls [BI82] discuss multiple inheritance in an extension to Smalltalk-80. Strongtalk [Sun06] is a strongly typed successor to Smalltalk developed at Sun Microsystems in the 1990s, and since released as open source. Gil and Sweeney [GS99] describe optimizations that can be used to reduce the time and space complexity of multiple inheritance.

Dolby [ Dol97 ] 描述了优化编译器如何识别嵌套对象可以扩展(在 Eiffel 意义上)的情况,同时保留引用语义。Bacon 和 Sweeney [ BS96 ] 以及 Diwan 等人 [ DMM96 ] 讨论了在编译时推断对象具体类型的技术,从而避免了 vtable 间接调用的开销。Driesen [ Dri93 ] 提出了一种 vtable 的替代方案,它需要进行全程序分析,但提供了极其高效的方法调度,即使在具有动态类型和多重继承的语言中也是如此。

Dolby [Dol97] describes how an optimizing compiler can identify circumstances in which a nested object can be expanded (in the Eiffel sense) while retaining reference semantics. Bacon and Sweeney [BS96] and Diwan et al. [DMM96] discuss techniques to infer the concrete type of objects at compile time, thereby avoiding the overhead of vtable indirection. Driesen [Dri93] presents an alternative to vtables that requires whole-program analysis, but provides extremely efficient method dispatch, even in languages with dynamic typing and multiple inheritance.

二进制组件系统允许将任意语言的任意编译器生成的代码组合成一个工作程序,通常跨越分布式机器集合。CORBA [ Sie00 ] 是由对象管理组织(一个由 700 多家公司组成的联盟)颁布的组件标准。.NET 是 Microsoft Corporation (microsoft.com/net) 的竞争标准,部分基于其早期的 ActiveX、DCOM 和 OLE [ Bro96 ] 产品。JavaBeans [ Sun97 ] 是用 Java 编写的符合 CORBA 的组件二进制标准。

Binary component systems allow code produced by arbitrary compilers for arbitrary languages to be joined together into a working program, often spanning a distributed collection of machines. CORBA [Sie00] is a component standard promulgated by the Object Management Group, a consortium of over 700 companies. .NET is a competing standard from Microsoft Corporation (microsoft.com/net), based in part on their earlier ActiveX, DCOM, and OLE [Bro96] products. JavaBeans [Sun97] is a CORBA-compliant binary standard for components written in Java.

随着 Web 服务的激增,分布式系统已被设计为以人类可读的形式交换和操作对象。SOAP [ Wor12 ] 最初是“简单对象访问协议”的缩写,是基于 Web 的信息传输和方法调用的标准。其底层数据通常编码为 XML(可扩展标记语言)[ Wor06a ]。近年来,SOAP 已基本被 REST(表述性状态转移)[ Fie00 ] 所取代,REST 是一套更为非正式的约定,位于普通 HTTP 之上。REST 中的底层数据可能采用多种形式 - 最常见的是 JSON(JavaScript 对象表示法)[ ECM13 ]。

With the explosion of web services, distributed systems have been designed to exchange and manipulate objects in human-readable form. SOAP [Wor12], originally an acronym for Simple Object Access Protocol, is a standard for web-based information transfer and method invocation. Its underlying data is typically encoded as XML (extensible markup language) [Wor06a]. In recent years, SOAP has largely been supplanted by REST (Representational State Transfer) [Fie00], a more informal set of conventions layered on top of ordinary HTTP. The underlying data in REST may take a variety of forms—most commonly JSON (JavaScript Object Notation) [ECM13].

面向对象编程领域的许多开创性论文都发表在ACM OOPSLA会议(面向对象编程系统、语言和应用程序)的论文集中,该会议自 1986 年以来每年举办一次,并作为ACM SIGPLAN 通知的特刊发表。Wegner [ Weg90 ] 列举了面向对象的定义特征。Meyer [ Mey92b,第 21.10 节] 解释了动态方法绑定的基本原理。Ungar 和 Smith [ US91 ] 描述了 Self,一种规范的基于对象(而不是基于类)的语言。

Many of the seminal papers in object-oriented programming have appeared in the proceedings of the ACM OOPSLA conferences (Object-Oriented Programming Systems, Languages, and Applications), held annually since 1986, and published as special issues of ACM SIGPLAN Notices. Wegner [Weg90] enumerates the defining characteristics of object orientation. Meyer [Mey92b, Sec. 21.10] explains the rationale for dynamic method binding. Ungar and Smith [US91] describe Self, the canonical object-based (as opposed to class-based) language.


1在前面的章节中,我们非正式地使用术语“对象”来指代几乎任何可以有名称的事物。在本章中,我们将仅使用它来指代类的实例。

1 In previous chapters we used the term “object” informally to refer to almost anything that can have a name. In this chapter we will use it only to refer to an instance of a class.

2克里斯汀·尼加德 (1926-2002) 是一位广受赞誉的数学家、计算机语言先驱和社会活动家。他的职业生涯包括在挪威国防研究机构、挪威运筹学会、挪威计算中心、奥胡斯大学和奥斯陆大学以及各种劳工、政治和社会组织任职。奥勒-约翰·达尔 (1931-2002) 也曾在挪威国防研究机构和挪威计算中心任职,并且是奥斯陆信息学系的创始人。尼加德和达尔共同获得了 2001 年 ACM 图灵奖。

2 Kristen Nygaard (1926–2002) was widely admired as a mathematician, computer language pioneer, and social activist. His career included positions with the Norwegian Defense Research Establishment, the Norwegian Operational Research Society, the Norwegian Computing Center, the Universities of Aarhus and Oslo, and a variety of labor, political, and social organizations. Ole-Johan Dahl (1931–2002) also held positions at the Norwegian Defense Research Establishment and the Norwegian Computing Center, and was the founding member of the Informatics department at Oslo. Together, Nygaard and Dahl shared the 2001 ACM Turing Award.

3派生类当然可以声明一个与某些现有成员同名的新成员,但两者将共存,如示例 10.10中所述。

3 A derived class can of course declare a new member with the same name as some existing member, but the two will then coexist, as discussed in Example 10.10.

4如果可用,编译器还可以使用移动构造函数(“R 值引用”,第 9.3.1 节)。为了避免过多混淆,我们在此将讨论限制在复制构造函数上。

4 The compiler may also use a move constructor (“R-value References,” Section 9.3.1), if available. To avoid excess confusion, we limit the discussion here to copy constructors.

5 C++ 在某些情况下还会使用virtual关键字在派生类声明的标头中作为基类名称的前缀。此用法支持共享多重继承的非常不同的目的,我们将在 C-10.6.3 节中讨论这一点。

5 C++ also uses the virtual keyword in certain circumstances to prefix the name of a base class in the header of the declaration of a derived class. This usage supports the very different purpose of shared multiple inheritance, which we will consider in Section C-10.6.3.

6其他语言的术语有所不同。在 Eiffel 中,接口被称为完全延迟类。在 Scala 中,它被称为特征

6 Terminology differs in other languages. In Eiffel, an interface is called a fully deferred class. In Scala, it's called a trait.

7艾伦·凯 (1940-) 于 1972 年加入 PARC。除了开发 Smalltalk 及其图形用户界面外,他还在笔记本电脑问世之前就构想并推广了笔记本电脑的概念。他于 1984 年成为苹果电脑的研究员,随后在迪士尼和惠普任职。他于 2003 年获得 ACM 图灵奖。

7 Alan Kay (1940–) joined PARC in 1972. In addition to developing Smalltalk and its graphical user interface, he conceived and promoted the idea of the laptop computer, well before it was feasible to build one. He became a Fellow at Apple Computer in 1984, and has subsequently held positions at Disney and Hewlett-Packard. He received the ACM Turing Award in 2003.

替代编程模型

Alternative Programming Models

替代编程模型

Alternative Programming Models

正如我们在第 1 章中提到的,编程语言传统上分为各种命令式和声明式语言,尽管这种分类并不完美。我们在第一部分和第二部分中曾提到过对每个主要语言系列特别重要的问题。此外,我们所涵盖的大部分内容(语法、语义、命名、类型、抽象)都适用于所有语言。不过,我们的注意力主要集中在主流命令式语言上。在第三部分中,我们将转移这一焦点。

As we noted in Chapter 1, programming languages are traditionally though imperfectly classified into various imperative and declarative families. We have had occasion in Parts I and II to mention issues of particular importance to each of the major families. Moreover much of what we have covered—syntax, semantics, naming, types, abstraction—applies uniformly to all. Still, our attention has focused mostly on mainstream imperative languages. In Part III we shift this focus.

函数式和逻辑语言是主要的非命令式选项。我们分别在第 11 章和12章中讨论它们。在每种情况下,我们都围绕代表性语言进行讨论:用于函数式编程的 Scheme 和 OCaml,用于逻辑编程的 Prolog。在第 11 章中,我们还介绍了急切求值和惰性求值,以及一等函数和高阶函数。在第12 章中,我们讨论了导致全自动通用逻辑编程困难的问题,并描述了在实践中用来保持模型易于处理的限制。这两章中的可选部分都考虑了数学基础:用于函数式编程的 Lambda 演算,用于逻辑编程的谓词演算。

Functional and logic languages are the principal nonimperative options. We consider them in Chapters 11 and 12, respectively. In each case we structure our discussion around representative languages: Scheme and OCaml for functional programming, Prolog for logic programming. In Chapter 11 we also cover eager and lazy evaluation, and first-class and higher-order functions. In Chapter 12 we cover issues that make fully automatic, general purpose logic programming difficult, and describe restrictions used in practice to keep the model tractable. Optional sections in both chapters consider mathematical foundations: Lambda Calculus for functional programming, Predicate Calculus for logic programming.

其余两章讨论了并发和脚本模型,这两种模型都越来越流行,并且跨越了命令式/声明式的界限。并发是由互联计算机的硬件并行性以及即将到来的多线程处理器和芯片级多处理器的爆炸式增长所驱动的。脚本是由万维网的增长和对程序员生产力的日益重视所驱动的,这种重视将快速开发和可重用性置于纯粹的运行时性能之上。

The remaining two chapters consider concurrent and scripting models, both of which are increasingly popular, and cut across the imperative/declarative divide. Concurrency is driven by the hardware parallelism of internetworked computers and by the coming explosion in multithreaded processors and chip-level multiprocessors. Scripting is driven by the growth of the World Wide Web and by an increasing emphasis on programmer productivity, which places rapid development and reusability above sheer run-time performance.

第 13 章从并发基础知识开始,包括通信和同步、线程创建语法以及线程的实现。本章的其余部分分为共享内存模型(其中线程使用显式或隐式同步机制来管理一组通用变量)和(在配套网站上)消息传递模型(其中线程仅通过显式通信进行交互)。

Chapter 13 begins with the fundamentals of concurrency, including communication and synchronization, thread creation syntax, and the implementation of threads. The remainder of the chapter is divided between shared-memory models, in which threads use explicit or implicit synchronization mechanisms to manage a common set of variables, and (on the companion site) message-passing models, in which threads interact only through explicit communication.

第 14 章的前半部分概述了脚本在其中发挥重要作用的问题领域:shell(命令)语言、文本处理和报告生成、数学和统计、程序组件的“粘合”、复杂应用程序的扩展机制以及客户端和服务器端 Web 脚本。后半部分讨论了脚本语言所倡导的一些更重要的语言创新:灵活的作用域和命名约定、字符串和模式操作(扩展正则表达式)以及高级数据类型。

The first half of Chapter 14 surveys problem domains in which scripting plays a major role: shell (command) languages, text processing and report generation, mathematics and statistics, the "gluing" together of program components, extension mechanisms for complex applications, and client and server-side Web scripting. The second half considers some of the more important language innovations championed by scripting languages: flexible scoping and naming conventions, string and pattern manipulation (extended regular expressions), and high level data types.

11

函数式语言

Functional Languages

本书前几章主要关注命令式编程语言。在本章和下一章中,我们将重点介绍函数式和逻辑语言。虽然命令式语言的使用范围更广,但函数式和逻辑语言都有“工业强度”的实现,并且这两种模型都有重要的商业应用。Lisp 传统上因处理符号数据而广受欢迎,尤其是在人工智能领域。OCaml 在金融服务行业得到广泛使用。近年来,函数式语言(尤其是静态类型的语言)在科学应用中也越来越受欢迎。逻辑语言广泛用于形式化规范和定理证明,还用于许多其他应用,但使用范围较窄。

Previous chapters of this text have focused largely on imperative programming languages. In the current chapter and the next we emphasize functional and logic languages instead. While imperative languages are far more widely used, “industrial-strength” implementations exist for both functional and logic languages, and both models have commercially important applications. Lisp has traditionally been popular for the manipulation of symbolic data, particularly in the field of artificial intelligence. OCaml is heavily used in the financial services industry. In recent years functional languages—statically typed ones in particular—have become increasingly popular for scientific applications as well. Logic languages are widely used for formal specifications and theorem proving and, less widely, for many other applications.

当然,函数式语言和逻辑语言与命令式语言有很多共同之处。每个模型下都会出现命名和范围问题。类型、表达式以及选择和递归的控制流概念也是如此。所有语言都必须进行语义扫描、解析和分析。此外,函数式语言大量使用子程序——甚至比大多数冯·诺依曼语言还要多——并且并发性和不确定性的概念在函数式语言和逻辑语言中与在命令式语言中一样常见。

Of course, functional and logic languages have a great deal in common with their imperative cousins. Naming and scoping issues arise under every model. So do types, expressions, and the control-flow concepts of selection and recursion. All languages must be scanned, parsed, and analyzed semantically. In addition, functional languages make heavy use of subroutines—more so even than most von Neumann languages—and the notions of concurrency and nondeterminacy are as common in functional and logic languages as they are in the imperative case.

如第 1 章所述,语言类别之间的界限往往相当模糊。人们可以在许多命令式语言中以大量函数式风格编写代码,许多函数式语言都包含命令式特性(赋值和迭代)。最常见的逻辑语言 Prolog 也提供了某些命令式特性。最后,在大多数函数式编程语言中,构建逻辑编程系统都很容易。

As noted in Chapter 1, the boundaries between language categories tend to be rather fuzzy. One can write in a largely functional style in many imperative languages, and many functional languages include imperative features (assignment and iteration). The most common logic language—Prolog—provides certain imperative features as well. Finally, it is easy to build a logic programming system in most functional programming languages.

由于命令式和函数式概念的重叠,我们在前几章中曾多次讨论过对函数式编程语言特别重要的问题。大多数此类语言严重依赖多态性(隐式参数类型 -第 7.1.2 节第 7.3 节第 7.2.4 节)。大多数语言大量使用列表(第 8.6 节)。从历史上看,有几个是动态范围的(第 3.3.6 节和 C-3.4.2 节)。所有语言都使用递归(第 6.6 节)进行重复执行,结果程序行为和性能在很大程度上取决于参数的评估规则(第 6.6.2 节)。所有这些都倾向于生成大量临时数据,其实现通过垃圾收集来回收这些数据(第 8.5.3 节)。

Because of the overlap between imperative and functional concepts, we have had occasion several times in previous chapters to consider issues of particular importance to functional programming languages. Most such languages depend heavily on polymorphism (the implicit parametric kind—Sections 7.1.2, 7.3, and 7.2.4). Most make heavy use of lists (Section 8.6). Several, historically, were dynamically scoped (Sections 3.3.6 and C-3.4.2). All employ recursion (Section 6.6) for repetitive execution, with the result that program behavior and performance depend heavily on the evaluation rules for parameters (Section 6.6.2). All have a tendency to generate significant amounts of temporary data, which their implementations reclaim through garbage collection (Section 8.5.3).

本章首先简要介绍命令式、函数式和逻辑编程模型的历史起源。然后,我们列举函数式编程的基本概念,并考虑如何在 Lisp 的 Scheme 方言和 ML 的 OCaml 方言中实现这些概念。简而言之,我们还考虑了 Common Lisp、Erlang、Haskell、Miranda、pH、单一赋值 C 和 Sisal。我们特别关注求值顺序和高阶函数的问题。对于那些对函数式编程的理论基础感兴趣的人,我们(在配套网站上)提供了函数、集合和 lambda 演算的介绍。形式主义有助于阐明纯函数式语言的概念,并阐明了实际语言与数学抽象的不同之处。

Our chapter begins with a brief introduction to the historical origins of the imperative, functional, and logic programming models. We then enumerate fundamental concepts in functional programming and consider how these are realized in the Scheme dialect of Lisp and the OCaml dialect of ML. More briefly, we also consider Common Lisp, Erlang, Haskell, Miranda, pH, Single Assignment C, and Sisal. We pay particular attention to issues of evaluation order and higher-order functions. For those with an interest in the theoretical foundations of functional programming, we provide (on the companion site) an introduction to functions, sets, and the lambda calculus. The formalism helps to clarify the notion of a pure functional language, and illuminates the places where practical languages diverge from the mathematical abstraction.

11.1 历史起源

11.1 Historical Origins

要理解不同编程模型之间的差异,了解它们的理论根源会有所帮助,因为它们都早于电子计算机的发展。命令式和函数式模型源自数学家阿兰·图灵、阿隆佐·丘奇、斯蒂芬·克莱恩、埃米尔·波斯特等人在 20 世纪 30 年代所做的工作。这些人基本上独立工作,基于自动机、符号运算、递归函数定义和组合学,开发了几种非常不同的算法或有效程序概念的形式化。随着时间的推移,这些不同的形式化被证明同样强大:任何可以在一种形式中计算的东西都可以在其他形式中计算。这个结果让丘奇猜想,任何直观吸引人的计算模型也同样强大;这个猜想被称为丘奇论点

To understand the differences among programming models, it can be helpful to consider their theoretical roots, all of which predate the development of electronic computers. The imperative and functional models grew out of work undertaken by mathematicians Alan Turing, Alonzo Church, Stephen Kleene, Emil Post, and others in the 1930s. Working largely independently, these individuals developed several very different formalizations of the notion of an algorithm, or effective procedure, based on automata, symbolic manipulation, recursive function definitions, and combinatorics. Over time, these various formalizations were shown to be equally powerful: anything that could be computed in one could be computed in the others. This result led Church to conjecture that any intuitively appealing model of computing would be equally powerful as well; this conjecture is known as Church's thesis.

图灵的计算模型是图灵机,这是一种让人联想到有限或下推自动机的自动机,但能够访问无限存储“磁带”的任意单元。1图灵机以命令式方式计算,通过更改磁带单元中的值,就像高级命令式程序通过更改变量的值进行计算一样。丘奇的计算模型称为 lambda 演算它基于参数化表达式的概念(每个参数由表达式的出现引入)。字母 λ——因此得名)。2 Lambda演算启发了函数式编程:人们使用它通过将参数代入表达式来进行计算,就像在高级函数式程序中通过将参数传递给函数来进行计算一样。Kleene 和 Post 的计算模型更加抽象,不适合直接作为编程语言实现。

Turing's model of computing was the Turing machine, an automaton reminiscent of a finite or pushdown automaton, but with the ability to access arbitrary cells of an unbounded storage “tape.”1 The Turing machine computes in an imperative way, by changing the values in cells of its tape, just as a high-level imperative program computes by changing the values of variables. Church's model of computing is called the lambda calculus. It is based on the notion of parameterized expressions (with each parameter introduced by an occurrence of the letter λ—hence the notation's name).2 Lambda calculus was the inspiration for functional programming: one uses it to compute by substituting parameters into expressions, just as one computes in a high-level functional program by passing arguments to functions. The computing models of Kleene and Post are more abstract, and do not lend themselves directly to implementation as a programming language.

可计算性早期研究的目标不是理解计算机(除了纯机械设备外,计算机并不存在),而是将有效程序的概念形式化。随着时间的推移,这项工作使数学家能够形式化构造性证明(展示如何获得具有某些所需属性的数学对象的证明)和非构造性证明(仅展示这样的对象必须存在,可能是通过矛盾、计数论证或归结为其他非构造性证明的定理)之间的区别。实际上,程序可以看作是以下命题的构造性证明:给定任何适当的输入,存在以特定期望方式与输入相关的输出。例如,欧几里得算法可以被认为是以下命题的构造性证明:每对非负整数都有一个最大公约数。

The goal of early work in computability was not to understand computers (aside from purely mechanical devices, computers did not exist) but rather to formalize the notion of an effective procedure. Over time, this work allowed mathematicians to formalize the distinction between a constructive proof (one that shows how to obtain a mathematical object with some desired property) and a nonconstructive proof (one that merely shows that such an object must exist, perhaps by contradiction, or counting arguments, or reduction to some other theorem whose proof is nonconstructive). In effect, a program can be seen as a constructive proof of the proposition that, given any appropriate inputs, there exist outputs that are related to the inputs in a particular, desired way. Euclid's algorithm, for example, can be thought of as a constructive proof of the proposition that every pair of non-negative integers has a greatest common divisor.

逻辑编程也与构造性证明的概念密切相关,但处于更抽象的层次。逻辑程序员不会编写适用于所有适当输入的通用构造性证明,而是编写一组公理,计算机能够每组特定的输入找到构造性证明。我们将在第 12 章中更详细地讨论逻辑编程。

Logic programming is also intimately tied to the notion of constructive proofs, but at a more abstract level. Rather than write a general constructive proof that works for all appropriate inputs, the logic programmer writes a set of axioms that allow the computer to discover a constructive proof for each particular set of inputs. We will consider logic programming in more detail in Chapter 12.

11.2 函数式编程概念

11.2 Functional Programming Concepts

从严格意义上讲,函数式编程将程序的输出定义为输入的数学函数,没有内部状态的概念,因此没有副作用。在我们这里考虑的语言中,Miranda、Haskell、pH、Sisal 和 Single Assignment C 是纯函数式的。Erlang 几乎如此。大多数其他语言都包含命令式特性。为了使函数式编程实用,函数式语言提供了许多命令式语言中经常缺少的特性,包括

In a strict sense of the term, functional programming defines the outputs of a program as a mathematical function of the inputs, with no notion of internal state, and thus no side effects. Among the languages we consider here, Miranda, Haskell, pH, Sisal, and Single Assignment C are purely functional. Erlang is nearly so. Most others include imperative features. To make functional programming practical, functional languages provide a number of features that are often missing in imperative languages, including

 一等函数值和高阶函数

 First-class function values and higher-order functions

 广泛的多态性

 Extensive polymorphism

 列表类型和运算符

 List types and operators

 结构化函数返回

 Structured function returns

 结构化对象的构造函数(聚合)

 Constructors (aggregates) for structured objects

 垃圾收集

 Garbage collection

第 3.6.2 节中,我们将一等值定义为可以作为参数传递、从子程序返回或(在具有副作用的语言中)赋给变量的值。严格解释该术语,一等状态还需要能够在运行时创建(计算)新值。对于子程序,这种一等状态的概念需要嵌套的lambda 表达式,这些表达式可以捕获在周围范围内定义的值,从而为这些值提供无限范围(即,即使其范围不再有效,它们仍保持活动状态)。子程序在大多数命令式语言中是二等值,但在所有函数式编程语言中都是一等值(严格意义上来说)。高阶函数函数作为参数,或返回函数作为结果。

In Section 3.6.2 we defined a first-class value as one that can be passed as a parameter, returned from a subroutine, or (in a language with side effects) assigned into a variable. Under a strict interpretation of the term, first-class status also requires the ability to create (compute) new values at run time. In the case of subroutines, this notion of first-class status requires nested lambda expressions that can capture values defined in surrounding scopes, giving those values unlimited extent (i.e., keeping them alive even after their scopes are no longer active). Subroutines are second-class values in most imperative languages, but first-class values (in the strict sense of the term) in all functional programming languages. A higher-order function takes a function as an argument, or returns a function as a result.

多态性在函数式语言中很重要,因为它允许将函数用于尽可能通用的一类参数。正如我们在7.17.2.4节中看到的,Lisp 及其方言是动态类型的,因此本质上是多态的,而 ML 及其相关语言则通过类型推断机制获得多态性。列表在函数式语言中很重要,因为它们具有自然的递归定义,并且可以通过操作其第一个元素并(递归地)操作列表的其余部分来轻松操作。递归很重要,因为在没有副作用的情况下,它是重复做任何事情的唯一方法。

Polymorphism is important in functional languages because it allows a function to be used on as general a class of arguments as possible. As we have seen in Sections 7.1 and 7.2.4, Lisp and its dialects are dynamically typed, and thus inherently polymorphic, while ML and its relatives obtain polymorphism through the mechanism of type inference. Lists are important in functional languages because they have a natural recursive definition, and are easily manipulated by operating on their first element and (recursively) the remainder of the list. Recursion is important because in the absence of side effects it provides the only means of doing anything repeatedly.

我们列出的函数式语言特性(递归、结构化函数返回、构造函数、垃圾收集)中的几项可以在部分(但不是全部)命令式语言中找到。Fortran 77 没有递归,也不允许从函数返回结构化类型(即数组)。Pascal 和早期版本的 Modula-2 只允许从函数返回简单类型和指针类型。正如我们在第 7.1.3 节中看到的,包括 Ada、C 和 Fortran 90 在内的几种命令式语言提供了允许在行内指定结构化值的聚合构造。然而,在大多数命令式语言中,这种构造是缺乏的或不完整的。C# 和几种脚本语言(其中包括 Python 和 Ruby)提供了能够表示(未命名的)函数值(lambda 表达式)的聚合,但很少有命令式语言具有如此丰富的表现力。纯函数式语言必须提供完全通用的聚合:因为没有办法更新现有对象,所以必须“一次性”初始化新创建的对象。最后,尽管垃圾收集在命令式语言中越来越常见,但它绝不是通用的,通常也不适用于子例程的局部变量,这些局部变量通常在堆栈中分配。由于希望为第一类函数和其他对象提供无限范围,函数式语言倾向于对所有动态分配的数据(或至少对所有编译器无法证明堆栈分配是安全的数据)采用(垃圾收集)堆。C++11 和 Java 8 提供 lambda 表达式,但没有无限范围。

Several of the items in our list of functional language features (recursion, structured function returns, constructors, garbage collection) can be found in some but not all imperative languages. Fortran 77 has no recursion, nor does it allow structured types (i.e., arrays) to be returned from functions. Pascal and early versions of Modula-2 allow only simple and pointer types to be returned from functions. As we saw in Section 7.1.3, several imperative languages, including Ada, C, and Fortran 90, provide aggregate constructs that allow a structured value to be specified in-line. In most imperative languages, however, such constructs are lacking or incomplete. C# and several scripting languages—Python and Ruby among them—provide aggregates capable of representing an (unnamed) functional value (a lambda expression), but few imperative languages are so expressive. A pure functional language must provide completely general aggregates: because there is no way to update existing objects, newly created ones must be initialized “all at once.” Finally, though garbage collection is increasingly common in imperative languages, it is by no means universal, nor does it usually apply to the local variables of subroutines, which are typically allocated in the stack. Because of the desire to provide unlimited extent for first-class functions and other objects, functional languages tend to employ a (garbage-collected) heap for all dynamically allocated data (or at least for all data for which the compiler is unable to prove that stack allocation is safe). C++11 and Java 8 provide lambda expressions, but without unlimited extent.

由于 Lisp 是最初的函数式语言,并且仍然是使用最广泛的语言之一,因此人们通常将 Lisp 的几个特性描述为与一般的函数式编程有关,尽管这种描述并不准确。我们将在第11.3 节中(在 Scheme 的上下文中)研究这些特性。它们包括

Because Lisp was the original functional language, and is still one of the most widely used, several characteristics of Lisp are commonly, though inaccurately, described as though they pertained to functional programming in general. We will examine these characteristics (in the context of Scheme) in Section 11.3. They include

 程序和数据的同质性​​:Lisp 中的程序本身就是一个列表,可以用操作数据的相同机制进行操作。

 Homogeneity of programs and data: A program in Lisp is itself a list, and can be manipulated with the same mechanisms used to manipulate data.

 自定义:Lisp 的操作语义可以通过用Lisp 编写的解释器来优雅地定义。

 Self-definition: The operational semantics of Lisp can be defined elegantly in terms of an interpreter written in Lisp.

 通过“读取-评估-打印”循环与用户交互。

 Interaction with the user through a “read-eval-print” loop.

许多程序员(可能是大多数程序员)都用命令式和函数式风格编写了大量软件,他们发现后者更美观。此外,各种大型商业项目的经验(参见本章末尾的参考书目注释)表明,没有副作用使得函数式程序比命令式程序更容易编写、调试和维护。当传递一组给定的参数时,纯函数总是可以返回相同的结果。未记录的副作用、错误顺序的更新以及悬空或(在大多数情况下)未初始化的引用等问题根本不会发生。同时,许多函数式语言的实现在可移植性、库包的丰富性、与其他语言的接口以及调试和分析工具方面仍然存在不足。我们将在第11.8 节中回到函数式和命令式编程之间的权衡。

Many programmers—probably most—who have written significant amounts of software in both imperative and functional styles find the latter more aesthetically appealing. Moreover, experience with a variety of large commercial projects (see the Bibliographic Notes at the end of the chapter) suggests that the absence of side effects makes functional programs significantly easier to write, debug, and maintain than their imperative counterparts. When passed a given set of arguments, a pure function can always be counted on to return the same results. Issues of undocumented side effects, misordered updates, and dangling or (in most cases) uninitialized references simply don't occur. At the same time, many implementations of functional languages still fall short in terms of portability, richness of library packages, interfaces to other languages, and debugging and profiling tools. We will return to the tradeoffs between functional and imperative programming in Section 11.8.

11.3 方案概述

11.3 A Bit of Scheme

Scheme 最初由 Guy Steele 和 Gerald Sussman 于 20 世纪 70 年代末开发,并经过多次修订。此处的描述遵循 1998 年 R5RS(第五次修订标准),也应符合 2013 年 R7RS。

Scheme was originally developed by Guy Steele and Gerald Sussman in the late 1970s, and has evolved through several revisions. The description here follows the 1998 R5RS (fifth revised standard), and should also be compliant with the 2013 R7RS.

例 11.1

Example 11.1

读取-求值-打印循环

The read-eval-print loop

大多数 Scheme 实现都使用运行“读取-求值-打印”循环的解释器。解释器反复从标准输入(通常由用户输入)读取表达式,计算该表达式,并打印结果值。如果用户输入

Most Scheme implementations employ an interpreter that runs a “read-eval-print” loop. The interpreter repeatedly reads an expression from standard input (generally typed by the user), evaluates that expression, and prints the resulting value. If the user types

(+ 3 4)

(+ 3 4)

解释器将会打印

the interpreter will print

7

7

如果用户输入

If the user types

7

7

解释器还会打印

the interpreter will also print

7

7

(数字7已经被完全评估。)为了节省程序员在键盘上逐字输入整个程序的需要,大多数 Scheme 实现都提供了一个load函数,可以读取(并评估)来自文件的输入:

(The number 7 is already fully evaluated.) To save the programmer the need to type an entire program verbatim at the keyboard, most Scheme implementations provide a load function that reads (and evaluates) input from a file:

(加载“我的方案”)

(load “my_Scheme_program”)

例 11.2

Example 11.2

括号的意义

Significance of parentheses

正如我们在6.1 节中提到的,Scheme(与所有 Lisp 方言一样)使用剑桥波兰表示法来表示表达式。括号表示函数应用(或在某些情况下表示使用宏)。左括号内的第一个表达式表示函数;其余表达式是其参数。假设用户输入

As we noted in Section 6.1, Scheme (like all Lisp dialects) uses Cambridge Polish notation for expressions. Parentheses indicate a function application (or in some cases the use of a macro). The first expression inside the left parenthesis indicates the function; the remaining expressions are its arguments. Suppose the user types

((+ 3 4))

((+ 3 4))

当解释器看到内括号时,它将调用函数+,并将34作为参数传递。由于外括号的存在,它将尝试将7作为零参数函数调用——这是一个运行时错误:

When it sees the inner set of parentheses, the interpreter will call the function +, passing 3 and 4 as arguments. Because of the outer set of parentheses, it will then attempt to call 7 as a zero-argument function—a run-time error:

eval: 7 不是一个程序

eval: 7 is not a procedure

与几乎所有其他编程语言的情况不同,额外的括号会改变 Lisp/Scheme 程序的语义:

Unlike the situation in almost all other programming languages, extra parentheses change the semantics of Lisp/Scheme programs:

(+ 3 4) ⇒ 7

(+ 3 4) ⇒ 7

((+ 3 4)) ⇒错误

((+ 3 4)) ⇒ error

此处的⇒ 表示“计算结果为”。此符号不是Scheme 本身语法的一部分。■

Here the ⇒ means “evaluates to.” This symbol is not a part of the syntax of Scheme itself. ■

例 11.3

Example 11.3

引用

Quoting

可以通过引用来阻止 Scheme 解释器评估括号内的表达式:

One can prevent the Scheme interpreter from evaluating a parenthesized expression by quoting it:

(引用 (+ 3 4))⇒ (+ 3 4)

(quote (+ 3 4)) ⇒ (+ 3 4)

此处的结果是一个三元素列表。更常见的是,引用是用一个特殊的简写符号来指定的,该符号由一个前导单引号组成:

Here the result is a three-element list. More commonly, quoting is specified with a special shorthand notation consisting of a leading single quote mark:

'(+ 3 4) ⇒ (+ 3 4)

'(+ 3 4) ⇒ (+ 3 4)

例 11.4

Example 11.4

动态类型

Dynamic typing

虽然在 Scheme 中每个表达式都有类型,但该类型通常直到运行时才确定。大多数预定义函数都会动态检查以确保其参数属于适当的类型。表达式

Though every expression has a type in Scheme, that type is generally not determined until run time. Most predefined functions check dynamically to make sure that their arguments are of appropriate types. The expression

(如果(> a 0)(+ 2 3)(+ 2 “foo”))

(if (> a 0) (+ 2 3) (+ 2 “foo”))

如果a为正数,则计算结果为5 ,但如果a为负数或零,则会产生运行时类型冲突错误。更重要的是,如第 7.1.2 节所述,对多种类型参数有意义的函数是隐式多态的:

will evaluate to 5 if a is positive, but will produce a run-time type clash error if a is negative or zero. More significantly, as noted in Section 7.1.2, functions that make sense for arguments of multiple types are implicitly polymorphic:

(定义最小值(lambda(ab)(如果(<ab)ab)))

(define min (lambda (a b) (if (< a b) a b)))

表达式(min 123 456)的计算结果为123(min 3.14159 2.71828)的计算结果为2.71828。■

The expression (min 123 456) will evaluate to 123; (min 3.14159 2.71828) will evaluate to 2.71828. ■

例 11.5

Example 11.5

类型谓词

Type predicates

用户定义函数可以使用预定义的类型谓词函数实现自己的类型检查:

User-defined functions can implement their own type checks using predefined type predicate functions:

(布尔值?x);xa 是布尔值吗?
(字符?x);xa 是字符吗?
(字符串?x);xa 是字符串吗?
(符号?x);xa 是符号吗?
(数字?x);xa 是一个数字吗?
(对?x);xa 是(不一定是合适的)对吗?
(列表?x);xa 是(正确)列表吗?

(这不是一份详尽的清单。)■

(This is not an exhaustive list.) ■

例 11.6

Example 11.6

符号的自由语法

Liberal syntax for symbols

Scheme 中的符号与其他语言中的标识符类似。标识符的词汇规则因 Scheme 实现而异,但通常比其他语言中的规则宽松得多。具体而言,标识符可以包含各种标点符号:

A symbol in Scheme is comparable to what other languages call an identifier. The lexical rules for identifiers vary among Scheme implementations, but are in general much looser than they are in other languages. In particular, identifiers are permitted to contain a wide variety of punctuation marks:

(符号?'x$_%:&=*!) ⇒ #t

(symbol? 'x$_%:&=*!) ⇒ #t

符号#t表示布尔值 true。false 用#f表示。请注意此处使用引号 ( ' );该符号以x开头。■

The symbol #t represents the Boolean value true. False is represented by #f. Note the use here of quote ( ' ); the symbol begins with x. ■

例 11.7

Example 11.7

lambda表达式

lambda expressions

要在 Scheme 中创建一个函数,需要求一个lambda 表达式3

To create a function in Scheme one evaluates a lambda expression:3

(lambda (x) (* xx)) ⇒ 函数

(lambda (x) (* x x)) ⇒ function

lambda的第一个“参数”是函数的形式参数列表(在本例中是单个参数x)。其余的“参数”(在本例中只有一个)构成函数主体。正如我们将在11.5 节中看到的那样,Scheme 区分了函数和所谓的特殊形式lambda就是其中之一,它与函数类似,但具有特殊的求值规则。严格地说,只有函数才有参数,但我们也将非正式地使用该术语来指代以特殊形式看起来像参数的子表达式。■

The first “argument” to lambda is a list of formal parameters for the function (in this case the single parameter x). The remaining “arguments” (again just one in this case) constitute the body of the function. As we shall see in Section 11.5, Scheme differentiates between functions and so-called special forms (lambda among them), which resemble functions but have special evaluation rules. Strictly speaking, only functions have arguments, but we will also use the term informally to refer to the subexpressions that look like arguments in a special form. ■

Lambda表达式不为其函数命名;这可以使用letdefine来完成(将在下一小节中介绍)。从这个意义上讲,lambda表达式类似于我们在7.1.3 节中用来指定数组或记录值的聚合。

A lambda expression does not give its function a name; this can be done using let or define (to be introduced in the next subsection). In this sense, a lambda expression is like the aggregates that we used in Section 7.1.3 to specify array or record values.

例 11.8

Example 11.8

功能评估

Function evaluation

当调用函数时,语言实现会恢复lambda表达式求值时有效的引用环境(与所有具有静态作用域和一流嵌套子例程的语言一样,Scheme 采用深度绑定)。然后,它使用形式参数的绑定来扩充此环境,并按顺序求值函数体的表达式。最后一个这样的表达式(通常只有一个)的值成为函数返回的值:

When a function is called, the language implementation restores the referencing environment that was in effect when the lambda expression was evaluated (like all languages with static scope and first-class, nested subroutines, Scheme employs deep binding). It then augments this environment with bindings for the formal parameters and evaluates the expressions of the function body in order. The value of the last such expression (most often there is only one) becomes the value returned by the function:

((lambda (x) (* xx)) 3) ⇒ 9

((lambda (x) (* x x)) 3) ⇒ 9

例 11.9

Example 11.9

if表达式

if expressions

可以使用if编写简单的条件表达式:

Simple conditional expressions can be written using if:

(如果(<23)45)⇒4

(if (< 2 3) 4 5) ⇒ 4

(如果 #f 2 3)⇒ 3

(if #f 2 3) ⇒ 3

通常,Scheme 表达式按应用顺序求值,如第 6.6.2 节所述。lambdaif等特殊形式是此规则的例外。if实现会检查第一个参数的求值是否为#t 。如果是,则返回第二个参数的值,而不求第三个参数的值。否则,它返回第三个参数的值,而不求第二个参数的值。我们将在第 11.5 节中回到求值顺序问题。■

In general, Scheme expressions are evaluated in applicative order, as described in Section 6.6.2. Special forms such as lambda and if are exceptions to this rule. The implementation of if checks to see whether the first argument evaluates to #t. If so, it returns the value of the second argument, without evaluating the third argument. Otherwise it returns the value of the third argument, without evaluating the second. We will return to the issue of evaluation order in Section 11.5. ■

11.3.1 绑定

11.3.1 Bindings

例 11.10

Example 11.10

使用let嵌套作用域

Nested scopes with let

可以通过引入嵌套范围将名称绑定到值:

Names can be bound to values by introducing a nested scope:

(设((a 3)

(let ((a 3)

  (b4)

  (b 4)

  (平方(lambda(x)(* xx)))

  (square (lambda (x) (* x x)))

  (加号 +)

  (plus +))

 (sqrt (加 (平方 a) (平方 b)))) ⇒ 5.0

 (sqrt (plus (square a) (square b)))) ⇒ 5.0

特殊形式let接受两个或更多参数。第一个参数是一对参数的列表。在每一对参数中,第一个元素是名称,第二个元素是该名称在let的其余参数中要表示的值。然后按顺序评估其余参数;整个构造的值是最后一个参数的值。

The special form let takes two or more arguments. The first of these is a list of pairs. In each pair, the first element is a name and the second is the value that the name is to represent within the remaining arguments to let. Remaining arguments are then evaluated in order; the value of the construct as a whole is the value of the final argument.

let产生的绑定的范围仅仅是let的第二个参数:

The scope of the bindings produced by let is let's second argument only:

(设((a 3))

(let ((a 3))

 (设((a 4)

 (let ((a 4)

   (吧)

   (b a))

  (+ ab)))   ⇒ 7

  (+ a b)))   ⇒ 7

这里b取外部 a的值。名称在声明列表末尾“一次性”可见的方式排除了递归函数的定义。对于这些,可以使用letrec

Here b takes the value of the outer a. The way in which names become visible “all at once” at the end of the declaration list precludes the definition of recursive functions. For these one employs letrec:

(letrec((事实

(letrec ((fact

  (λ(n)

  (lambda (n)

   (如果(= n 1)1

   (if (= n 1) 1

    (* n (事实 (- n 1))))))

    (* n (fact (- n 1)))))))

 (事实 5))⇒ 120

 (fact 5))      ⇒ 120

还有一个let*构造,其中名称“一次一个”可见,以便后面的名称可以使用前面的名称,但反之则不行。■

There is also a let* construct in which names become visible “one at a time” so that later ones can make use of earlier ones, but not vice versa. ■

例 11.11

Example 11.11

使用define进行全局绑定

Global bindings with define

如第 3.3 节所述,Scheme 是静态作用域的。(Common Lisp 也是静态作用域的。大多数其他 Lisp 方言都是动态作用域的。)虽然letletrec允许用户创建嵌套作用域,但它们不会影响全局名称(Scheme 解释器最外层已知的名称)的含义。对于这些,Scheme 提供了一种称为define的特殊形式,其副作用是为名称创建全局绑定:

As noted in Section 3.3, Scheme is statically scoped. (Common Lisp is also statically scoped. Most other Lisp dialects are dynamically scoped.) While let and letrec allow the user to create nested scopes, they do not affect the meaning of global names (names known at the outermost level of the Scheme interpreter). For these Scheme provides a special form called define that has the side effect of creating a global binding for a name:

(定义假设

(define hypot

 (λ(ab)

 (lambda (a b)

  (sqrt (+ (* aa) (* bb)))))

  (sqrt (+ (* a a) (* b b)))))

(假设 3 4)⇒ 5

(hypot 3 4)      ⇒ 5

11.3.2 列表和数字

11.3.2 Lists and Numbers

例 11.12

Example 11.12

基本列表操作

Basic list operations

与所有 Lisp 方言一样,Scheme 提供了大量函数来操作列表。我们在8.6 节中看到了很多这样的函数;我们在这里就不一一重复了。其中最重要的三个是car,它返回列表的头部;cdr (“coulder”) ,它返回列表的其余部分(头部之后的所有内容);以及cons,它将头部与列表的其余部分连接起来:

Like all Lisp dialects, Scheme provides a wealth of functions to manipulate lists. We saw many of these in Section 8.6; we do not repeat them all here. The three most important are car, which returns the head of a list, cdr (“coulder”), which returns the rest of the list (everything after the head), and cons, which joins a head to the rest of a list:

(汽车'(2 3 4))⇒ 2

(car '(2 3 4)) ⇒ 2

(cdr'(2 3 4))⇒(3 4)

(cdr ' (2 3 4)) ⇒ (3 4)

(反对 2 '(3 4)) ⇒ (2 3 4)

(cons 2 '(3 4)) ⇒ (2 3 4)

同样有用的还有null?谓词,它确定其参数是否为空列表。回想一下,符号'(2 3 4)表示列表,其中最后一个元素是空列表:

Also useful is the null? predicate, which determines whether its argument is the empty list. Recall that the notation '(2 3 4) indicates a proper list, in which the final element is the empty list:

(cdr'(2))⇒()

(cdr '(2)) ⇒ ()

(cons 2 3) ⇒ (2.3) ;不正确的列表

(cons 2 3) ⇒ (2.3) ; an improper list

为了快速访问序列中的任意元素,Scheme 提供了一种向量类型,该类型由整数索引,就像数组一样,并且可以包含异构类型的元素,就像记录一样。感兴趣的读者可以参阅 Scheme 手册 [ SDF + 07 ] 以了解更多信息。

For fast access to arbitrary elements of a sequence, Scheme provides a vector type that is indexed by integers, like an array, and may have elements of heterogeneous types, like a record. Interested readers are referred to the Scheme manual [SDF+07] for further information.

Scheme 还提供了大量的数值和逻辑(布尔)函数和特殊形式。语言手册描述了五种数值类型的层次结构:整数有理数实数复数数字。最后两个级别是可选的:实现可以选择不提供任何非实数。大多数但并非所有实现都采用整数和有理数的任意精度表示,后者在内部存储为(分子,分母)对。

Scheme also provides a wealth of numeric and logical (Boolean) functions and special forms. The language manual describes a hierarchy of five numeric types: integer, rational, real, complex, and number. The last two levels are optional: implementations may choose not to provide any numbers that are not real. Most but not all implementations employ arbitrary-precision representations of both integers and rationals, with the latter stored internally as (numerator, denominator) pairs.

11.3.3 相等性测试和搜索

11.3.3 Equality Testing and Searching

Scheme 提供了几种不同的相等性测试函数。对于数值比较,=在必要时执行类型转换(例如,比较整数和浮点数)。对于一般用途,eqv?执行比较,而equal?执行深度(递归)比较,在叶子节点处使用eqv? 。eq?函数也执行浅比较,在某些情况下可能比eqv?更便宜(特别是,eq?不需要检测存储在不同位置的离散值的相等性,尽管在某些实现中可能需要)。第 7.4 节介绍了更多详细信息。

Scheme provides several different equality-testing functions. For numerical comparisons, = performs type conversions where necessary (e.g., to compare an integer and a floating-point number). For general-purpose use, eqv? performs a shallow comparison, while equal? performs a deep (recursive) comparison, using eqv? at the leaves. The eq? function also performs a shallow comparison, and may be cheaper than eqv? in certain circumstances (in particular, eq? is not required to detect the equality of discrete values stored in different locations, though it may in some implementations). Further details were presented in Section 7.4.

例 11.13

Example 11.13

列表搜索功能

List search functions

为了在列表中搜索元素,Scheme 提供了两组函数,每组都有与三个通用相等谓词相对应的变体。函数memqmemvmember以元素和列表为参数,并返回以该元素开头的列表的最长后缀(如果有):

To search for elements in lists, Scheme provides two sets of functions, each of which has variants corresponding to the three general-purpose equality predicates. The functions memq, memv, and member take an element and a list as argument, and return the longest suffix of the list (if any) beginning with the element:

(memq'z'(xyzw))⇒ (zw)
(memv'(z)'(xy(z)w))⇒ #f;(等价于?'(z)'(z))⇒#f
(成员‘(z)’(xy(z)w))⇒ ((z)w) ;(相等?'(z)'(z))⇒#t

memq 、memvmember函数分别使用eq?eqv?equal?进行比较。如果未找到所需元素,它们将返回#f。事实证明,Scheme 的条件表达式(例如if )将除#f之外的任何内容视为真。4因此,人们经常会看到以下形式的表达式

The memq, memv, and member functions perform their comparisons using eq?, eqv?, and equal?, respectively. They return #f if the desired element is not found. It turns out that Scheme's conditional expressions (e.g., if) treat anything other than #f as true.4 One therefore often sees expressions of the form

(如果(memq 所需元素列表可能包含它)...

(if (memq desired-element list-that-might-contain-it) …

例 11.14

Example 11.14

搜索关联列表

Searching association lists

函数assqassvassoc在关联列表(也称为A 列表)中搜索值。A 列表是在 C-3.4.2 节中在具有动态作用域的语言的名称查找上下文中引入的。A 列表是一个以对的列表形式实现的字典。5对的第一个元素是某种键;第二个元素是与该键相对应的信息。Assq 、assvassoc键和 A 列表为参数,并返回列表中的第一个对(如果有),其第一个元素分别是键的eq?eqv?equal?。如果没有匹配的对,则返回#f 。■

The functions assq, assv, and assoc search for values in association lists (otherwise known as A-lists). A-lists were introduced in Section C-3.4.2 in the context of name lookup for languages with dynamic scoping. An A-list is a dictionary implemented as a list of pairs.5 The first element of each pair is a key of some sort; the second element is information corresponding to that key. Assq, assv, and assoc take a key and an A-list as argument, and return the first pair in the list, if there is one, whose first element is eq?, eqv?, or equal?, respectively, to the key. If there is no matching pair, #f is returned. ■

11.3.4 控制流和赋值

11.3.4 Control Flow and Assignment

例 11.15

Example 11.15

多路条件表达式

Multiway conditional expressions

我们已经见过特殊形式if。它有一个名为cond的表亲,类似于更通用的if…elsif…else

We have already seen the special form if. It has a cousin named cond that resembles a more general if… elsif… else:

(条件

(cond

 ((< 3 2) 1)

 ((< 3 2) 1)

 ((< 4 3) 2)

 ((< 4 3) 2)

 (否则 3))   ⇒ 3

 (else 3))   ⇒ 3

cond的参数是成对的。它们按从第一个到最后一个的顺序考虑。整个表达式的值是第一对中第二个元素的值,其中第一个元素的计算结果为#t。如果第一个元素的计算结果都不为#t,则整个值为#f。符号else仅允许作为构造的最后一对的第一个元素,它充当#t 的语法糖。■

The arguments to cond are pairs. They are considered in order from first to last. The value of the overall expression is the value of the second element of the first pair in which the first element evaluates to #t. If none of the first elements evaluates to #t, then the overall value is #f. The symbol else is permitted only as the first element of the last pair of the construct, where it serves as syntactic sugar for #t. ■

当然,递归是 Scheme 中重复执行操作的主要方法。第 6.6 节讨论了许多与递归相关的问题;我们在此不再重复讨论。

Recursion, of course, is the principal means of doing things repeatedly in Scheme. Many issues related to recursion were discussed in Section 6.6; we do not repeat that discussion here.

例 11.16

Example 11.16

任务

Assignment

对于希望利用副作用的程序员,Scheme 提供了赋值、排序和迭代构造。赋值采用特殊形式set!以及函数set-car!set-cdr !:

For programmers who wish to make use of side effects, Scheme provides assignment, sequencing, and iteration constructs. Assignment employs the special form set! and the functions set-car! and set-cdr!:

(let ((x 2)       ; 将 x 初始化为 2

(let ((x 2)      ; initialize x to 2

  (l '(ab)))       ; 将 l 初始化为 (ab)

  (l '(a b)))      ; initialize l to (a b)

 (set! x 3)       ;将 x 赋值为 3

 (set! x 3)      ; assign x the value 3

 (set-car! l '(cd))       ; 将 l 的头部赋值为 (cd)

 (set-car! l '(c d))      ; assign head of l the value (c d)

 (set-cdr! l '(e))       ; 将 l 的其余部分赋值为 (e)

 (set-cdr! l '(e))      ; assign rest of l the value (e)

 … x⇒3       ​

 … x       ⇒ 3

 … l        ⇒ ((cd) e)

 … l       ⇒ ((c d) e)

各种 set !的返回值与实现相关。■

The return values of the various varieties of set! are implementation-dependent. ■

例 11.17

Example 11.17

测序

Sequencing

排序使用特殊形式begin

Sequencing uses the special form begin:

(开始

(begin

 (显示“hi”)

 (display “hi “)

 (显示“妈妈”)

 (display “mom”))

这里我们使用了begin来对显示表达式进行排序,这会导致解释器打印它们的参数。■

Here we have used begin to sequence display expressions, which cause the interpreter to print their arguments. ■

例 11.18

Example 11.18

迭代

Iteration

迭代使用特殊形式do和函数for-each

Iteration uses the special form do and the function for-each:

(定义 iter-fib

(define iter-fib

 (λ(n)

 (lambda (n)

  ; 打印前 n+1 个斐波那契数

  ; print the first n+1 Fibonacci numbers

  (do ((i 0 (+ i 1))      ;最初为 0,在每次迭代中增加

  (do ((i 0 (+ i 1))     ; initially 0, inc'ed in each iteration

    (a 0 b)      ;最初为 0,在每次迭代中设置为 b

    (a 0 b)     ; initially 0, set to b in each iteration

    (b 1 (+ ab)))      ;初始为 1,设置为 a 与 b 之和

    (b 1 (+ a b)))     ; initially 1, set to sum of a and b

   ((= in) b)      ;终止测试和最终值

   ((= i n) b)     ; termination test and final value

   (显示 b)      ;循环体

   (display b)     ; body of loop

   (显示“ “))))     ;循环体

   (display “ “))))     ; body of loop

(每个

(for-each

 (lambda (ab) (显示 (* ab)) (换行符))

 (lambda (a b) (display (* a b)) (newline))

 '(2 4 6)

 '(2 4 6)

 '(3 5 7))

 '(3 5 7))

do的第一个参数是三元组列表,每个三元组指定一个新变量、该变量的初始值以及在每次迭代结束时要计算并放置在变量的新实例中的表达式。do 的第二个参数是一,指定终止条件和要返回的表达式。在每次迭代结束时,将使用当前值计算循环变量(例如ab)的所有新值。只有在计算完所有新值后,才会创建新的变量实例。

The first argument to do is a list of triples, each of which specifies a new variable, an initial value for that variable, and an expression to be evaluated and placed in a fresh instance of the variable at the end of each iteration. The second argument to do is a pair that specifies the termination condition and the expression to be returned. At the end of each iteration all new values of loop variables (e.g., a and b) are computed using the current values. Only after all new values are computed are the new variable instances created.

for-each函数以一个函数和一个列表序列作为参数。列表的数量必须与函数所接受的参数数量相同,并且列表必须所有的长度都相同。For-each重复调用其函数参数,从列表中传递连续的参数集。在此处显示的示例中,lambda 表达式生成的未命名函数将在参数 2 和 3、4 和 5 以及 6 和 7 上被调用。解释器将打印

The function for-each takes as argument a function and a sequence of lists. There must be as many lists as the function takes arguments, and the lists must all be of the same length. For-each calls its function argument repeatedly, passing successive sets of arguments from the lists. In the example shown here, the unnamed function produced by the lambda expression will be called on the arguments 2 and 3,4 and 5, and 6 and 7. The interpreter will print

6

6

20

20

四十二

42

()

()

最后一行是for-each的返回值,这里假设为空列表。语言定义允许此值依赖于实现;构造的执行是为了其副作用。■

The last line is the return value of for-each, assumed here to be the empty list. The language definition allows this value to be implementation-dependent; the construct is executed for its side effects. ■

设计与实现

Design & Implementation

11.1 函数式程序中的迭代

11.1 Iteration in functional programs

区分迭代作为重复执行的符号和迭代作为安排副作用的手段非常重要。事实上,可以将迭代定义为尾部递归的语法糖,而 Val、Sisal 和 pH 正是这样做的(使用特殊语法来促进将值从一次迭代传递到下一次迭代)。这样的符号可能仍然完全没有副作用,即完全是功能性的。在 Scheme 中,赋值和 I/O 是真正必要的特性。我们认为迭代是必要的,因为大多数使用它的 Scheme 程序在其循环中都有赋值或 I/O。

It is important to distinguish between iteration as a notation for repeated execution and iteration as a means of orchestrating side effects. One can in fact define iteration as syntactic sugar for tail recursion, and Val, Sisal, and pH do precisely that (with special syntax to facilitate the passing of values from one iteration to the next). Such a notation may still be entirely side-effect free, that is, entirely functional. In Scheme, assignment and I/O are the truly imperative features. We think of iteration as imperative because most Scheme programs that use it have assignments or I/O in their loops.

前面的章节中提到了另外两种控制流构造。延迟强制第 6.6.2 节)允许对表达式进行惰性求值。使用当前继续调用call/cc第 6.2.2 节)允许以闭包的形式保存当前程序计数器和引用环境,并将其传递给指定的子例程。我们将在第 11.5 节中再次提到延迟强制

Two other control-flow constructs have been mentioned in previous chapters. Delay and force (Section 6.6.2) permit the lazy evaluation of expressions. Call-with-current-continuation (call/cc; Section 6.2.2) allows the current program counter and referencing environment to be saved in the form of a closure, and passed to a specified subroutine. We will mention delay and force again in Section 11.5.

11.3.5 程序作为列表

11.3.5 Programs as Lists

现在应该清楚了,Scheme 中的程序采用列表的形式。从技术术语上讲,我们说 Lisp 和 Scheme 是同像的— 自我表示。无论我们将其视为程序还是列表,带括号的符号字符串(其中括号是匹配的)都称为S 表达式。事实上,未求值的程序一个列表,可以使用所有常用的列表函数构造、解构和以其他方式操作。

As should be clear by now, a program in Scheme takes the form of a list. In technical terms, we say that Lisp and Scheme are homoiconic—self-representing. A parenthesized string of symbols (in which parentheses are balanced) is called an S-expression regardless of whether we think of it as a program or as a list. In fact, an unevaluated program is a list, and can be constructed, deconstructed, and otherwise manipulated with all the usual list functions.

例 11.19

Example 11.19

将数据评估为代码

Evaluating data as code

正如quote可用于抑制对函数调用中作为参数出现的列表的评估一样,S​​cheme 提供了一个eval函数,可用于评估作为数据结构创建的列表:

Just as quote can be used to inhibit the evaluation of a list that appears as an argument in a function call, Scheme provides an eval function that can be used to evaluate a list that has been created as a data structure:

(定义撰写

(define compose

 (λ(fg)

 (lambda (f g)

  (λ (x) (f (gx)))))

  (lambda (x) (f (g x)))))

((编写汽车 cdr)'(1 2 3) )        ⇒2

((compose car cdr) '(1 2 3))       ⇒ 2

(定义 compose2

(define compose2

 (λ(fg)

 (lambda (f g)

  (eval (列表'lambda'(x) (列表 f (列表 g'x)))

  (eval (list 'lambda '(x) (list f (list g 'x)))

   (方案报告环境 5))))

   (scheme-report-environment 5))))

((compose2 汽车 cdr)'(1 2 3) )        ⇒2

((compose2 car cdr) '(1 2 3))       ⇒ 2

在第一个声明中,compose接受一对函数fg作为参数。它返回一个函数,该函数接受一个值x作为参数,并应用g 对其,然后应用f,最后返回结果。在第二个声明中,compose2执行相同的功能,但方式不同。函数列表返回由其(已评估的)参数组成的列表。在compose2的主体中,此列表是未评估的表达式(lambda (x) (f (gx)))。当传递给eval时,此列表将评估为所需的函数。eval的第二个参数指定要在其中评估表达式的引用环境。在我们的示例中,我们指定了由 Scheme 版本 5 报告 [ KCR + 98 ]定义的环境。■

In the first of these declarations, compose takes as arguments a pair of functions f and g. It returns as result a function that takes as parameter a value x, applies g to it, then applies f, and finally returns the result. In the second declaration, compose2 performs the same function, but in a different way. The function list returns a list consisting of its (evaluated) arguments. In the body of compose2, this list is the unevaluated expression (lambda (x) (f (gx))). When passed to eval, this list evaluates to the desired function. The second argument of eval specifies the referencing environment in which the expression is to be evaluated. In our example we have specified the environment defined by the Scheme version 5 report [KCR+98]. ■

Lisp [ MAE + 65 ]的原始描述包括该语言的自我定义:用 Lisp 编写的 Lisp 解释器代码。尽管 Scheme 在很多方面都与早期的 Lisp 不同(最明显的是其对词法作用域的使用),但仍然可以轻松编写这样的元循环解释器 [ AS96第 4 章]。代码基于函数evalapply。其中第一个我们刚刚看到过。第二个,apply,接受两个参数:一个函数和一个列表。它实现了调用函数的效果,并使用列表的元素作为参数。

The original description of Lisp [MAE+65] included a self-definition of the language: code for a Lisp interpreter, written in Lisp. Though Scheme differs in many ways from this early Lisp (most notably in its use of lexical scoping), such a metacircular interpreter can still be written easily [AS96, Chap. 4]. The code is based on the functions eval and apply. The first of these we have just seen. The second, apply, takes two arguments: a function and a list. It achieves the effect of calling the function, with the elements of the list as arguments.

11.3.6 扩展示例:Scheme 中的 DFA 模拟

11.3.6 Extended Example: DFA Simulation in Scheme

例 11.20

Example 11.20

在 Scheme 中模拟 DFA

Simulating a DFA in Scheme

在结束对 Scheme 的介绍之前,我们提供了一个完整的程序来模拟 DFA(确定性有限自动机)的执行。代码如图11.1所示。有限自动机的详细信息可以在第 2.2 节和 C-2.4.1 节中找到。在这里,我们将 DFA 表示为一个包含三个项目的列表:起始状态、转换函数和最终状态列表。转换函数又由一串对表示。每对的第一个元素都是另一对,其第一个元素是状态,第二个元素是输入符号。如果当前状态和下一个输入符号与一对的第一个元素匹配,则有限自动机进入该对的第二个元素给出的状态。

To conclude our introduction to Scheme, we present a complete program to simulate the execution of a DFA (deterministic finite automaton). The code appears in Figure 11.1. Finite automata details can be found in Sections 2.2 and C-2.4.1. Here we represent a DFA as a list of three items: the start state, the transition function, and a list of final states. The transition function in turn is represented by a list of pairs. The first element of each pair is another pair, whose first element is a state and whose second element is an input symbol. If the current state and next input symbol match the first element of a pair, then the finite automaton enters the state given by the second element of the pair.

f11-01-9780124104099
图 11.1 Scheme 程序模拟 DFA 的动作。i,函数 move 搜索从起始状态到某个新状态s的标记为i 的转换。然后,它返回一个具有相同转换函数和最终状态的新机器,但以s作为其“起始”状态。主函数mock封装了一个尾部递归辅助函数,该函数累积一个移动的倒排列表,并在使用完所有输入符号后返回。然后,包装器检查辅助函数是否以最终状态结束;它返回(正确排序的)一系列移动,最后接受拒绝。函数 cadr 和 caddr 分别定义为(lambda (x) (car (cdr x)))(lambda (x) (car (cdr (cdr x)))) Scheme 提供了大量这样的缩写。

为了具体说明这一点,请考虑图 11.2中的 DFA 。它接受所有由 0 和 1 组成的字符串,其中每个数字出现偶数次。为了模拟该机器,我们将它与输入字符串一起传递给函数模拟。在运行时,自动机将其所经过的状态的踪迹累积为列表。一旦输入耗尽,它就会添加acceptrejection。例如,如果我们输入

To make this concrete, consider the DFA of Figure 11.2. It accepts all strings of zeros and ones in which each digit appears an even number of times. To simulate this machine, we pass it to the function simulate along with an input string. As it runs, the automaton accumulates as a list a trace of the states through which it has traveled. Once the input is exhausted, it adds accept or reject. For example, if we type

f11-02-9780124104099
图 11.2 DFA 接受所有包含偶数个 的零和一的字符串。图的底部是机器作为 Scheme 数据结构的表示,使用了图 11.1的约定。

(模拟

(simulate

 零一偶数-dfa    ;机器描述

 zero-one-even-dfa   ; machine description

 '(0 1 1 0 1))    ;输入字符串

 '(0 1 1 0 1))   ; input string

然后 Scheme 解释器将会打印

then the Scheme interpreter will print

(q0 q2 q3 q2 q0 q1 拒绝)

(q0 q2 q3 q2 q0 q1 reject)

如果我们将输入字符串更改为010010,解释器将打印

If we change the input string to 010010, the interpreter will print

(q0 q2 q3 q1 q3 ​​q2 q0 接受)

(q0 q2 q3 q1 q3 q2 q0 accept)

11-01-9780124104099检查你的理解

Check Your Understanding

1. 函数式编程背后的数学形式是什么?

1. What mathematical formalism underlies functional programming?

2. 列出函数式编程语言的几个显著特征。

2. List several distinguishing characteristics of functional programming languages.

3.简要描述 Lisp/Scheme 读取-求值-打印循环的行为。

3. Briefly describe the behavior of the Lisp/Scheme read-eval-print loop.

4. 什么是一等价值?

4. What is a first-class value?

5.解释 Scheme 中letlet*letrec之间的区别。

5. Explain the difference between let, let*, and letrec in Scheme.

6.解释 eq?eqv?equal?之间的区别。

6. Explain the difference between eq?, eqv?, and equal?.

7. 描述 Scheme 程序脱离纯函数式编程模型的三种方式。

7. Describe three ways in which Scheme programs can depart from a purely functional programming model.

8. 什么是关联列表

8. What is an association list?

9.一种语言的 同音异义是什么意思?

9. What does it mean for a language to be homoiconic?

10. 什么是S 表达式

10. What is an S-expression?

11.概述 evalapply的行为。

11. Outline the behavior of eval and apply.

11.4 一点 OCaml

11.4 A Bit of OCaml

和 Lisp 一样,ML 也有着复杂的家族史。最初的语言是由 Robin Milner 和剑桥大学的其他人在 20 世纪 70 年代初发明的。SML(“标准” ML)和 OCaml(Objective Caml)是当今使用最广泛的两种方言。Haskell 是函数式编程研究中使用最广泛的语言,它是 ML 的一个独立后代(通过 Miranda)。由微软和其他公司开发的 F# 是 OCaml 的后代。

Like Lisp, ML has a complicated family tree. The original language was devised in the early 1970s by Robin Milner and others at Cambridge University. SML (“Standard” ML) and OCaml (Objective Caml) are the two most widely used dialects today. Haskell, the most widely used language for functional programming research, is a separate descendant of ML (by way of Miranda). F#, developed by Microsoft and others, is a descendant of OCaml.

自 20 世纪 80 年代初以来,法国国家计算研究机构 INRIA 的研究人员一直领导着 OCaml(及其前身 Caml)的开发工作(“O”是在名称中添加的,以引入面向对象特性)在 20 世纪 90 年代初)。在 ML 家族语言中,OCaml 以 INRIA 实现的效率和广泛的商业应用而闻名:在其他领域中,OCaml 在金融行业很受欢迎。

Work on OCaml (and its predecessor, Caml) has been led since the early 1980s by researchers at INRIA, the French national computing research organization (the 'O' was added to the name with the introduction of object-oriented features in the early 1990s). Among the ML family languages, OCaml is known for the efficiency of the INRIA implementation and for its widespread commercial adoption: among other domains, OCaml is popular in the finance industry.

INRIA OCaml 发行版包含字节码编译器(附带虚拟机)和针对各种机器架构优化的本机代码编译器。解释器可以交互使用,也可以执行以前编写的程序。学习该语言的最简单方法是交互地试用解释器。本节其余部分中的示例均在该环境中运行。

The INRIA OCaml distribution includes both a byte-code compiler (with accompanying virtual machine) and an optimizing native-code compiler for a variety of machine architectures. The interpreter can be used either interactively or to execute a previously written program. The easiest way to learn the language is to experiment with the interpreter interactively. The examples in the remainder of this section all work in that environment.

例 11.21

Example 11.21

与解释器交互

Interacting with the interpreter

解释器反复从标准输入读取表达式,计算该表达式,并打印结果值。如果用户输入

The interpreter repeatedly reads an expression from standard input, evaluates that expression, and prints the resulting value. If the user types

3 + 4;;

3 + 4;;

解释器将会打印

the interpreter will print

- :int = 7

- : int = 7

双分号用于表示“顶级形式”的结束——最外层范围内的表达式。输出表明用户的表达式 ( - ) 是一个整数 7。

Double semicolons are used to indicate the end of a “top-level form”—an expression in the outermost scope. The output indicates that the user's expression (-) was an integer of value 7.

如果用户输入

If the user types

7;;

7;;

解释器还会打印

the interpreter will also print

- :int = 7

- : int = 7

(数字 7 已经被完全评估。)程序员可以指示解释器从文件中加载预先存在的代码,而不是直接在解释器中输入预先存在的代码:

(The number 7 is already fully evaluated.) Rather than type preexisting code into the interpreter directly, the programmer can instruct the interpreter to load it from a file:

#使用“mycode.ml”;;

#use “mycode.ml”;;

开头的井号表示这是对解释器的指令,而不是要求值的表达式。■

The initial hash sign indicates that this is a directive to the interpreter, rather than an expression to be evaluated. ■

例 11.22

Example 11.22

函数调用语法

Function call syntax

要调用一个函数,需要输入函数名称及其参数:

To invoke a function, one types the function name followed by its argument(s):

余弦0.0;;⇒ 1.0
最少 3 4;;⇒ 3

这里cos需要一个实数参数;min需要两个相同类型的参数,它们必须支持排序比较。与我们在 Scheme 中的介绍一样,我们使用 ⇒ 作为简写来表示求值结果。

Here cos expects a single real-number argument; min expects two arguments of the same type, which must support comparison for ordering. As in our coverage of Scheme, we use ⇒ as shorthand to indicate the result of evaluation.

注意函数调用中没有括号!调用仅通过并列表示。诸如foo (3, 4)之类的表达式不会foo应用于两个参数 3 和 4,而是应用于元组 (3, 4)。(元组本质上是一种记录,其元素是位置性的,而不是命名性的;有关这方面的更多信息,请参见第 11.4.3 节。)■

Note the absence of parentheses in function calls! Invocation is indicated simply by juxtaposition. An expression such as foo (3, 4) does not apply foo to the two arguments 3 and 4, but rather to the tuple (3, 4). (A tuple is essentially a record whose elements are positional rather than named; more on this in Section 11.4.3.) ■

例 11.23

Example 11.23

函数值

Function values

如果我们输入cos这个名字

If we type in the name cos all by itself

余弦;;

cos;;

OCaml 告诉我们我们的表达式是一个从floatfloat的函数:

OCaml informs us that our expression is a function from floats to floats:

- :浮点型 -> 浮点型 = <fun>

- : float -> float = <fun>

如果我们询问(+ )(我们必须将其括在括号中以避免语法错误),我们会了解到它是一个将两个整数映射到第三个整数的函数:

If we ask about (+) (which we must enclose in parentheses to avoid a syntax error), we learn that it is a function that maps two integers to a third:

- :int -> int -> int = <fun>

- : int -> int -> int = <fun>

如果我们询问min,我们会发现它是多态的:

If we ask about min, we learn that it is polymorphic:

- : 'a -> 'a -> 'a = <fun>

- : 'a -> 'a -> 'a = <fun>

如第 7.2.4 节所述, ' a类型参数;它表示min的参数和结果类型可以是任意的,只要它们相同即可(当然,由于min在内部使用< ,如果 ' a是函数类型,我们将遇到运行时异常)。■

As explained in Section 7.2.4, the 'a is a type parameter; it indicates that the argument and result types of min can be arbitrary, so long as they are the same (of course, since min uses < internally, we will suffer a run-time exception if 'a is a function type). ■

例 11.24

Example 11.24

单位类型

unit type

函数调用中缺少括号确实提出了一个问题:我们如何区分简单命名值和零参数函数调用?答案是坚持要求此类函数采用空括号表示的虚拟占位符参数。那么,对没有(有用)参数的函数的调用看起来很像对 C 中的零参数函数的调用:

The lack of parentheses in function calls does raise the question: how do we distinguish a simple named value from a call to a zero-argument function? The answer is to insist that such functions take a dummy, placeholder argument, indicated by empty parentheses. A call to a function with no (useful) arguments then looks much like a call to a zero-argument function in C:

让 c_three = 3;;

let c_three = 3;;

让 f_three() = 3;;

let f_three () = 3;;

这里c_three是 int 类型的常量;f_three是unit -> int类型的函数。前者可以在任何需要整数的上下文中使用;后者在使用unit参数调用时返回一个整数:

Here c_three is a constant of type int; f_three is a function of type unit -> int. The former can be used in any context that expects an integer; the latter returns an integer when called with a unit argument:

c_三;;⇒ 3
f_three();;⇒ 3

OCaml 中的词汇约定很简单:标识符由大小写字母、数字、下划线和单引号组成;大多数标识符必须以小写字母或下划线开头(少数特殊名称,包括类型构造函数、变体、模块和异常,必须以大写字母开头)。注释以 ( * … * ) 分隔,并允许嵌套。浮点数必须包含小数点:表达式cos 0将生成类型冲突错误消息。

Lexical conventions in OCaml are straightforward: Identifiers are composed of upper- and lower-case letters, digits, underscores, and single quote marks; most are required to start with a lower-case letter or underscore (a few special kinds of names, including type constructors, variants, modules, and exceptions, must start with an upper-case letter). Comments are delimited with (* … *), and are permitted to nest. Floating-point numbers are required to contain a decimal point: the expression cos 0 will generate a type-clash error message.

内置类型包括布尔值、整数、浮点数、字符和字符串。可以使用各种类型构造函数创建更复杂类型的值,包括列表、数组、元组、记录、变体、对象和类;其中几种在11.4.3 节中描述。​​如7.2.4 节所述,类型检查是通过推断每个表达式的类型来执行的,然后检查每当两个表达式需要是同一类型时(例如,因为一个是参数,另一个是相应的形式参数),推断结果是否相同。为了支持类型推断,一些在其他语言中重载的运算符在 OCaml 中是分开的。特别是,通常的算术运算有整数(+、−、*、/)和浮点数(+.、−.、*.、/.)版本。

Built-in types include Boolean values, integers, floating-point numbers, characters, and strings. Values of more complex types can be created using a variety of type constructors, including lists, arrays, tuples, records, variants, objects, and classes; several of these are described in Section 11.4.3. As discussed in Section 7.2.4, type checking is performed by inferring a type for every expression, and then checking that whenever two expressions need to be of the same type (e.g., because one is an argument and the other is the corresponding formal parameter), the inferences turn out to be the same. To support type inference, some operators that are overloaded in other languages are separate in OCaml. In particular, the usual arithmetic operations have both integer (+, −, *, /) and floating-point (+., −., *., /.) versions.

11.4.1 相等和排序

11.4.1 Equality and Ordering

例 11.25

Example 11.25

“物理”与“结构”比较

“Physical” and “structural” comparison

和大多数函数式语言一样,OCaml 使用引用模型来表示名称。当比较两个表达式时(其中一个或两个都可能只是一个名称),有两种不同的相等概念。所谓的“物理”比较器==!=执行我们在第 7.4 节中所说的“浅”比较:它们确定表达式是否引用同一个对象(广义上)。所谓的“结构”比较器=<>执行我们所谓的“深”比较:它们确定表达式引用的对象是否具有相同的内部结构或行为。因此,以下表达式的计算结果均为true

Like most functional languages, OCaml uses a reference model for names. When comparing two expressions, either or both of which may simply be a name, there are two different notions of equality. The so-called “physical” comparators, == and !=, perform what we called a “shallow” comparison in Section 7.4: they determine if the expressions refer to the same object, in the broad sense of the word. The so-called “structural” comparators, = and <>, perform what we called a “deep” comparison: they determine if the objects to which the expressions refer have the same internal structure or behavior. Thus the following expressions all evaluate to true:

物理(浅)结构(深层)
2 == 22 = 2
“foo” != “foo”“foo” = “foo”
[1; 2; 3] != [1; 2; 3][1; 2; 3] = [1; 1+1; 5−2]

在第一行中,世界上(概念上)只有一个 2,因此对它的引用在物理和结构上都是等效的。在第二行中,两个具有相同组成字符的字符串在结构上等效,但在物理上不等效。在第三行中,两个列表即使看起来语法相同,但在物理上也是不同的;如果它们的对应元素在结构上等效,则它们在结构上等效。值得注意的是,值为函数的表达式可以进行比较以进行物理(浅)相等性比较,但如果进行比较以进行结构相等性比较,则会导致运行时异常(函数的等效行为是不可判定问题)。循环结构的结构比较可能导致无限循环。■

In the first line, there is (conceptually) only one 2 in the world, so references to it are both physically and structurally equivalent. In the second line, two character strings with the same constituent characters are structurally but not physically equivalent. In the third line, two lists are physically different even if they look syntactically the same; they are structurally equivalent if their corresponding elements are structurally equivalent. Significantly, expressions whose values are functions can be compared for physical (shallow) equality, but cause a run-time exception if compared for structural equality (equivalent behavior for functions is an undecidable problem). Structural comparison of cyclic structures can result in an infinite loop. ■

排序比较(<><=>=)始终基于深度比较。它在 OCaml 中针对函数以外的所有类型都进行了定义。它对算术类型、字符和字符串(后者按字典顺序工作)执行通常预期的操作;对于其他类型,结果是确定性的但不一定直观。在所有情况下,结果都与结构相等性测试(=)一致:如果a = b,则a <= ba >= b;如果a <> b,则a < ba > b。与相等性测试一样,函数比较将导致运行时异常;循环结构的比较可能不会终止。

Comparison for ordering (<, >, <=, >=) is always based on deep comparison. It is defined in OCaml on all types other than functions. It does what one would normally expect on arithmetic types, characters, and strings (the latter works lexicographically); on other types the results are deterministic but not necessarily intuitive. In all cases, the results are consistent with the structural equality test (=): if a = b, then a <= b and a >= b; if a <> b, then a < b or a > b. As with the equality tests, comparison of functions will cause a run-time exception; comparison of cyclic structures may not terminate.

11.4.2 绑定和 Lambda 表达式

11.4.2 Bindings and Lambda Expressions

例 11.26

Example 11.26

最外层声明

Outermost declarations

OCaml 中的新名称用let引入。最外层(顶层)的let引入的名称在其文件或模块的其余部分中可见:

New names in OCaml are introduced with let. An outermost (top-level) let introduces a name that is visible throughout the remainder of its file or module:

让平均值 = fun xy -> (x +. y) /. 2.;;

let average = fun x y -> (x +. y) /. 2.;;

这里fun引入了一个 lambda 表达式。右箭头 ( -> )前面的名称是参数;箭头后面的表达式是函数体 — 它将返回的值。考虑到函数声明的普遍性,为了使程序更具可读性,OCaml 提供了以下稍微简单的语法糖:

Here fun introduces a lambda expression. The names preceding the right arrow (->) are parameters; the expression following the arrow is the body of the function—the value it will return. To make programs a bit more readable, given the ubiquity of function declarations, OCaml provides the following somewhat simpler syntactic sugar:

设计与实现

Design & Implementation

11.2 SML 和 Haskell 中的相等性和排序

11.2 Equality and ordering in SML and Haskell

与 OCaml 不同,SML 提供了一个相等运算符:一个内置多态函数,定义在某些类型上,但不是所有类型。相等测试对于不可变类型的表达式来说很深,对于可变类型的表达式来说很浅。对不受支持的类型(例如函数)的测试会产生编译时错误消息,而不是运行时异常。相比之下,排序比较被定义为内置函数集合的重载名称,每个函数都适用于不同的类型。

Unlike OCaml, SML provides a single equality operator: a built-in polymorphic function defined on some but not all types. Equality tests are deep for expressions of immutable types and shallow for those of mutable types. Tests on unsupported (e.g., function) types produce a compile-time error message rather than a run-time exception. The ordering comparisons, by contrast, are defined as overloaded names for a collection of built-in functions, each of which works on a different type.

如例 3.28和侧边栏 7.7所述,Haskell 使用类型类的概念统一并扩展了相等和比较的处理。例如,相等运算符(=<> )在预定义类Eq中声明(但未定义);传递给这些运算符之一的任何值都将被推断为Eq类中的某种类型。传递给排序运算符(<<=>=>)之一的任何值都将被推断为Ord类中的某种类型。后者类被定义为Eq的扩展;Ord类中的每种类型也必须支持Eq类的运算符。类型类和具有混合继承的语言的接口(第 10.5 节)之间存在很强的类比。

As noted in Example 3.28 and Sidebar 7.7, Haskell unifies and extends the handling of equality and comparisons with a concept known as type classes.The equality operators (= and <>), for example, are declared (but not defined) in a predefined class Eq; any value that is passed to one of these operators will be inferred to be of some type in class Eq. Any value that is passed to one of the ordering operators (<, <=, >=, >) will similarly be inferred to be of some type in class Ord. This latter class is defined to be an extension of Eq; every type in class Ord must support the operators of class Eq as well. There is a strong analogy between type classes and the interfaces of languages with mix-in inheritance (Section 10.5).

设平均值 xy = (x +. y) /. 2.;;

let average x y = (x +. y) /. 2.;;

在任一版本的函数声明中,xy都将被推断为浮点数,因为它们是用浮点数+ . 运算符相加的。如果需要,程序员可以明确记录这一点:

In either version of the function declaration, x and y will be inferred to be floats, because they are added with the floating-point +. operator. The programmer can document this explicitly if desired:

让平均值:float -> float -> float = fun xy -> (x +. y) /. 2.;;

let average: float -> float -> float = fun x y -> (x +. y) /. 2.;;

或者

or

让平均值 (x:float) (y:float) float = (x +. y) /. 2.;;

let average (x:float) (y:float) float = (x +. y) /. 2.;;

例 11.27

Example 11.27

嵌套声明

Nested declarations

嵌套作用域使用let…in…结构创建。要根据三角形边长计算三角形面积,我们可以使用以下基于海伦公式的函数:

Nested scopes are created with the let…in… construct. To compute the area of a triangle given the lengths of its sides, we might use the following function based on Heron's formula:

让三角形面积 abc =

let triangle_area a b c =

 设 s = (a +. b +. c) /. 2.0

 let s = (a +. b +. c) /. 2.0 in

 sqrt(s*.(s−.a)*.(s−.b)*.(s−.c));;

 sqrt (s *. (s−.a) *. (s−.b) *. (s−.c));;

这里s是in后面的表达式的局部变量。它既不会在triple_area函数外部可见,也不会在其自身定义体(内部=in之间的表达式)中可见。■

Here s is local to the expression following the in. It will be neither visible outside the triangle_area function nor in the body of its own definition (the expression between the inner = and the in). ■

例 11.28

Example 11.28

递归嵌套函数(重复示例 7.38

A recursive nested function (reprise of Example 7.38)

当然,在递归的情况下,我们确实需要一个在声明中可见的函数:

In the case of recursion, of course, we do need a function to be visible within its declaration:

设 fib n =

let fib n =

 让 rec fib_helper f1 f2 i =

 let rec fib_helper f1 f2 i =

  如果 i = n 则 f2

  if i = n then f2

  否则 fib_helper f2 (f1 + f2) (i + 1) 在

  else fib_helper f2 (f1 + f2) (i + 1) in

 fib_helper 0 1 0;;

 fib_helper 0 1 0;;

这里fib_helper不仅在fib的主体内可见,而且在其自己的主体内也可见。■

Here fib_helper is visible not only within the body of fib, but also within its own body. ■

11.4.3 类型构造函数

11.4.3 Type Constructors

列表

Lists

例 11.29

Example 11.29

多态列表运算符

Polymorphic list operators

在大多数函数式语言中,程序员大量使用列表,OCaml 也不例外。列表具有天然的递归性,并适合用递归函数进行操作。在脚本语言和 Lisp 方言中,它们都是动态类型的,列表可以是异构的 - 单个列表可能包含多个任意类型的值。在 ML 及其后代中,它们在编译时执行所有检查,列表必须是同质的 - 所有元素必须具有相同的类型。同时,操纵列表而不对其成员执行操作的函数可以将任何类型的列表作为参数 - 它们具有天然的多态性:

Programmers make heavy use of lists in most functional languages, and OCaml is no exception. Lists are naturally recursive, and lend themselves to manipulation with recursive functions. In scripting languages and dialects of Lisp, all of which are dynamically typed, lists can be heterogeneous—a single list may contain values of multiple, arbitrary types. In ML and its descendants, which perform all checking at compile time, lists must be homogeneous—all elements must have the same type. At the same time, functions that manipulate lists without performing operations on their members can take any kind of list as argument—they are naturally polymorphic:

让 rec 附加 l1 l2 =

let rec append l1 l2 =

 如果 l1 = [] 那么 l2

 if l1 = [] then l2

 否则 hd l1 ::附加(tl l1)l2;;

 else hd l1 :: append (tl l1) l2;;

让 rec 成员 xl =

let rec member x l =

 如果 l = [] 那么 false

 if l = [] then false

 否则,如果 x = hd l,则为真

 else if x = hd l then true

 else 成员 x (tll);;

 else member x (tl l);;

这里append的类型为'a list -> 'a list -> 'a list;member的类型为'a -> 'a list -> bool。空括号([])表示空列表。内置的::构造函数类似于Lisp 中的cons:它接受一个元素和一个列表,并将前者添加到后者的开头;其类型为'a -> 'a list -> 'a listhdtl函数类似于Lisp 中的carcdr :它们分别返回由::创建的列表的头和余数。它们与许多其他有用的例程(包括append)一起由标准List库导出。 (事实证明,在 OCaml 中,使用hdtl通常被认为是不好的形式。由于它们只对非空列表起作用,所以两个函数都必须在运行时检查它们的参数并准备抛出异常。我们将在第 11.4.4 节中介绍的OCaml 的模式匹配机制允许在编译时执行检查,并且几乎总是提供更好的编写代码的方法。)■

Here append is of type 'a list -> 'a list -> 'a list; member is of type 'a -> 'a list -> bool. Empty brackets ([]) represent the empty list. The built-in :: constructor is analogous to cons in Lisp: it takes an element and a list and tacks the former onto the beginning of the latter; its type is 'a -> 'a list -> 'a list. The hd and tl functions are analogous to car and cdr in Lisp: they return the head and the remainder, respectively, of a list created by ::. They are exported—together with many other useful routines (including append)—by the standard List library. (As it turns out, use of hd and tl is generally considered bad form in OCaml. Because they work only on nonempty lists, both functions must check their argument at run time and be prepared to throw an exception. OCaml's pattern matching mechanism, which we will examine in Section 11.4.4, allows the checking to be performed at compile time, and almost always provides a better way to write the code.) ■

例 11.30

Example 11.30

列表符号

List notation

OCaml 中的列表是不可变的:一旦创建,其内容就永远不会改变。列表聚合通常使用“方括号”表示法编写,用分号分隔内部元素。表达式[a; b; c]与a :: b :: c :: []相同。请注意,如果abc都是同一类型(称之为 ' t),表达式a :: b :: c仍会生成类型冲突错误消息:第二个::的右侧操作数需要是 ' t 列表类型,而不仅仅是 ' t

Lists in OCaml are immutable: once created, their content never changes. List aggregates are most often written using “square bracket” notation, with semicolons separating the internal elements. The expression [a; b; c] is the same as a :: b :: c :: []. Note that if a, b, and c are all of the same type (call it 't), the expression a :: b :: c will still generate a type-clash error message: the right-hand operand of the second :: needs to be of type 't list, not just 't.

内置的@符号构造函数的行为类似于append的中缀版本。表达式[a; b; c] @ [d; e; f; g]与append [a; b; c] [d; e; f; g]相同;其计算结果为[a; b; c; d; e; f; g]

The built-in at-sign constructor, @, behaves like an infix version of append. The expression [a; b; c] @ [d; e; f; g] is the same as append [a; b; c] [d; e; f; g]; it evaluates to [a; b; c; d; e; f; g].

由于 OCaml 列表是同质的,有人可能会对[]的类型感到疑惑。为了使其与任何列表兼容,它被赋予类型 '列表。■

Since OCaml lists are homogeneous, one might wonder about the type of []. To make it to be compatible with any list, it is given type 'a list. ■

数组和字符串

Arrays and Strings

虽然列表具有自然的递归定义和动态可变的长度,但它们的不变性和线性时间访问成本(对于任意元素)使它们对于许多应用程序来说并不理想。因此,OCaml 提供了一种更传统的数组类型。数组的长度在制定时(即在运行时遇到其声明时)是固定的,但其元素可以在常量时间内访问,并且它们的值可以通过命令式代码更改。

While lists have a natural recursive definition and dynamically variable length, their immutability and linear-time access cost (for an arbitrary element) make them less than ideal for many applications. OCaml therefore provides a more conventional array type. The length of an array is fixed at elaboration time (i.e., when its declaration is encountered at run time), but its elements can be accessed in constant time, and their values can be changed by imperative code.

例 11.31

Example 11.31

数组表示法

Array notation

数组聚合看起来很像列表,但是在方括号内有竖线:

Array aggregates look much like lists, but with vertical bars immediately inside the square brackets:

让 five_primes = [| 2; 3; 5; 7; 11 |];;

let five_primes = [| 2; 3; 5; 7; 11 |];;

数组索引始终从零开始。使用.()运算符访问元素:

Array indexing always starts at zero. Elements are accessed using the .() operator:

五个素数。(2);;     ⇒5

five_primes.(2);;     ⇒ 5

与列表不同,数组是可变的。使用左箭头赋值运算符进行更新:

Unlike lists, arrays are mutable. Updates are made with the left-arrow assignment operator:

五个素数。(2)<-4;;     ⇒()

five_primes.(2) <-4;;     ⇒ ()

五个素数。(2);;      ⇒4

five_primes.(2);;     ⇒ 4

请注意,赋值本身返回单位值;它是根据其副作用进行评估的。■

Note that the assignment itself returns the unit value; it is evaluated for its side effect. ■

例 11.32

Example 11.32

字符串作为字符数组

Strings as character arrays

字符串本质上是字符数组。它们用双引号分隔,并使用.[]运算符进行索引:

Strings are essentially arrays of characters. They are delimited with double quotes, and indexed with the .[] operator:

让greeting = “嗨,妈妈!”;;

let greeting = “hi, mom!”;;

问候。[7];;      ⇒ '!'

greeting.[7];;     ⇒ '!'

从 OCaml 4.02 开始,字符串默认是不可变的,但有一个相关的字节类型支持更新:

As of OCaml 4.02, strings are immutable by default, but there is a related bytes type that supports updates:

让询问 = Bytes.of_string 问候;;

let enquiry = Bytes.of_string greeting;;

Bytes.set 查询 7 '?';;   ⇒ ()

Bytes.set enquiry 7 '?';;  ⇒ ()

询问;;       ⇒ “嗨,妈妈?”

enquiry;;      ⇒ “hi, mom?”

元组和记录

Tuples and Records

例 11.33

Example 11.33

元组表示法

Tuple notation

我们在示例 11.22中简要提到过元组,它们是不可变的、异构的、但大小固定的简单类型值集合。元组聚合体用逗号分隔组件值并用括号括起来。在化学数据库中,元素汞可以用元组(“Hg”, 80, 200.592)表示,表示元素的化学符号、原子序数和标准原子量。这个元组的类型为string * int * float ;星号表示乘法,反映了元组值是从stringintfloat域的笛卡尔积中提取的事实。

Tuples, which we mentioned briefly in Example 11.22, are immutable, heterogeneous, but fixed-size collections of values of simpler types. Tuple aggregates are written by separating the component values with commas and surrounding them with parentheses. In a chemical database, the element Mercury might be represented by the tuple (“Hg”, 80, 200.592), representing the element's chemical symbol, atomic number, and standard atomic weight. This tuple is said to be of type string * int * float; the stars, suggestive of multiplication, reflect the fact that tuple values are drawn from the Cartesian product of the string, int, and float domains.

元组的组成部分通常通过模式匹配提取(第 11.4.4 节)。在二元素元组(通常称为)中,也可以使用内置多态函数fstsnd来获取组成部分:

Components of tuples are typically extracted via pattern matching (Section 11.4.4). In two-element tuples (often referred to as pairs), the components can also be obtained using the built-in polymorphic functions fst and snd:

fst (“汞”,80);;    ⇒ “汞”

fst (“Hg”, 80);;   ⇒ “Hg”

snd (“Hg”,80) ;;    ⇒80 ■

snd (“Hg”, 80);;   ⇒ 80

例 11.34

Example 11.34

记录符号

Record notation

记录与元组非常相似,但组成值(字段)是有名称的,而不是位置的。语言实现必须为记录的内部表示选择一个顺序,但这个顺序对程序员来说是不可见的。要将字段名称引入编译器,必须声明每个记录类型:

Records are much like tuples, but the component values (fields) are named, rather than positional. The language implementation must choose an order for the internal representation of a record, but this order is not visible to the programmer. To introduce field names to the compiler, each record type must be declared:

类型元素 =

type element =

 {名称:字符串;原子序数:整数;原子重量:浮点数};;

 {name: string; atomic_number: int; atomic_weight: float};;

记录聚合用括号括起来,字段(任意顺序)用分号分隔:

Record aggregates are enclosed in braces, with the fields (in any order) separated by semicolons:

让水星 =

let mercury =

 {原子序数 = 80; 名称 = “Hg”; 原子量 = 200.592};;

 {atomic_number = 80; name = “Hg”; atomic_weight = 200.592};;

使用熟悉的“点”符号,可以通过名称轻松访问记录的各个字段:

Individual fields of a record are easily accessed by name, using familiar “dot” notation:

水星.原子重量;;   ⇒ 200.592

mercury.atomic_weight;;   ⇒ 200.592

例 11.35

Example 11.35

可变字段

Mutable fields

根据程序员的判断,字段可以声明为可变的:

At the programmer's discretion, fields can be declared to be mutable:

类型 sale_item = {名称:字符串;可变价格:浮点数};;

type sale_item = {name: string; mutable price: float};;

就像数组的元素一样,可以使用左箭头运算符更改可变字段:

Like elements of an array, mutable fields can then be changed with the left-arrow operator:

让 my_item = {name = “bike”; price = 699.95};;

let my_item = {name = “bike”; price = 699.95};;

我的商品.价格;;      ⇒ 699.95

my_item.price;;     ⇒ 699.95

我的商品.价格 <- 800.00;;      ⇒ ()

my_item.price <- 800.00;;     ⇒ ()

my_item;;         ⇒ {名称 = “自行车”; 价格 = 800。}

my_item;;        ⇒ {name = “bike”; price = 800.}

例 11.36

Example 11.36

参考

References

为方便起见,OCaml 标准库定义了一个多态引用类型,它本质上是具有单个可变字段的记录。感叹号运算符!用于检索引用所指的对象;:=用于赋值:

As a convenience, the OCaml standard library defines a polymorphic ref type that is essentially a record with a single mutable field. The exclamation-point operator ! is used to retrieve the object referred to by the reference; := is used for assignment:

让 x = ref 3;;
!x;;⇒ 3
x := !x + 1;;⇒()
!x;;⇒ 4

变体类型

Variant Types

例 11.37

Example 11.37

枚举类型的变体

Variants as enumerations

必须声明变体类型,如记录,但声明不是引入一组命名字段,每个字段都存在于类型的每个值中,而是引入一组命名构造函数(变体),其中一个将存在于类型的每个值中。在最简单的情况下,构造函数都只是名称,类型本质上是一个枚举:

Variant types, like records, must be declared, but instead of introducing a set of named fields, each of which is present in every value of the type, the declaration introduces a set of named constructors (variants), one of which will be present in each value of the type. In the simplest case, the constructors are all simply names, and the type is essentially an enumeration:

输入工作日 = 周日 | 周一 | 周二 | 周三 | 周四 | 周五 | 周六;;

type weekday = Sun | Mon | Tue | Wed | Thu | Fri | Sat;;

请注意,构造函数名称必须以大写字母开头。■

Note that constructor names must begin with a capital letter. ■

例 11.38

Example 11.38

变体作为联合体

Variants as unions

在更复杂的示例中,构造函数可以为其变体指定类型。总体类型本质上是一个联合:

In more complicated examples, a constructor may specify a type for its variant. The overall type is then essentially a union:

类型 yearday = YMD of int * int * int | YD of int * int;;

type yearday = YMD of int * int * int | YD of int * int;;

此代码将YMD定义为以三整数元组为参数的构造函数,将YD定义为以二整数元组为参数的构造函数。目的是允许将一年中的天数指定为 (年、月、日) 三元组或 (年、日) 对,其中对的第二个元素的范围为 1 到 366。在 2015 年(非闰年),7 月 4 日可以表示为YMD (2015, 7, 4)YD (2015, 185),但相等性测试YMD (2015, 7, 4) = YD (2015, 185)会失败。(如果需要,我们可以为此类构造值定义一个特殊的相等运算符 - 参见练习 11.16。)■

This code defines YMD as a constructor that takes a three-integer tuple as argument, and YD as a constructor that takes a two-integer tuple as argument. The intent is to allow days of the year to be specified either as (year, month, day) triples or as (year, day) pairs, where the second element of the pair may range from 1 to 366. In 2015 (a non–leap year), the Fourth of July could be represented eitheras YMD (2015, 7, 4) or as YD (2015, 185), though the equality test YMD (2015, 7, 4) = YD (2015, 185) would fail. (We could, if desired, define a special equality operator for such constructed values—see Exercise 11.16.) ■

例 11.39

Example 11.39

递归变体

Recursive variants

变体类型对于递归结构特别有用,其中不同的变体表示定义的基础部分和归纳部分。典型示例是二叉树:

Variant types are particularly useful for recursive structures, where different variants represent the base and inductive parts of a definition. The canonical example is a binary tree:

类型'a tree = Empty | 'a *'a tree *'a tree 的节点;;

type 'a tree = Empty | Node of 'a * 'a tree * 'a tree;;

根据这个定义,树

Given this definition, the tree

u11-01-9780124104099

可以写成Node ('R',Node('X',Empty,Empty),Node('Y',Node('Z',Empty,Empty),Node('W',Empty,Empty)))。■

can be written Node ('R', Node ('X', Empty, Empty), Node ('Y', Node ('Z', Empty, Empty), Node ('W', Empty, Empty))). ■

11.4.4 模式匹配

11.4.4 Pattern Matching

模式匹配(尤其是针对字符串的模式匹配)出现在许多编程语言中。示例包括 Snobol、Icon、Perl 以及采用了 Perl 功能的几种其他脚本语言(在14.4.2 节中讨论)。ML 的独特之处在于将模式匹配扩展到所有构造值(包括元组、列表、记录和变体),并将其与静态类型和类型推断集成在一起。

Pattern matching, particularly for strings, appears in many programming languages. Examples include Snobol, Icon, Perl, and the several other scripting languages that have adopted Perl's facilities (discussed in Section 14.4.2). ML is distinctive in extending pattern matching to the full range of constructed values—including tuples, lists, records, and variants—and integrating it with static typing and type inference.

例 11.40

Example 11.40

参数模式匹配

Pattern matching of parameters

OCaml 中的一个简单示例是传递参数时。例如,假设我们需要一个函数从表示为元组的元素中提取原子序数:

A simple example in OCaml occurs when passing parameters. Suppose, for example, that we need a function to extract the atomic number from an element represented as a tuple:

让原子序数(s,n,w)=n;;

let atomic_number (s, n, w) = n;;

设汞=(“Hg”,80,200.592);;

let mercury = (“Hg”, 80, 200.592);;

原子序数汞;;     ⇒80

atomic_number mercury;;     ⇒ 80

这里,atomic_number的参数mercury已与函数定义中的 (single, tuple) 参数匹配,为我们提供了其各个字段的名称,我们只需返回其中一个字段即可。由于函数主体中未使用其他两个字段,因此我们实际上不必为它们命名:“通配符” (_)模式可以代替使用:

Here mercury, the argument to atomic_number, has been matched against the (single, tuple) parameter in the function definition, giving us names for its various fields, one of which we simply return. Since the other two fields are unused in the body of the function, we don't really have to give them names: the “wild card” (_) pattern can be used instead:

让原子数(_,n,_)= n;;

let atomic_number (_, n, _) = n;;

例 11.41

Example 11.41

局部声明中的模式匹配

Pattern matching in local declarations

声明本地名称时,模式匹配也有效:

Pattern matching also works when declaring local names:

让原子序数 e =

let atomic_number e =

 让(_,n,_)= e 在 n 中;;

 let (_, n, _) = e in n;;

例 11.42

Example 11.42

match构造

The match construct

在atomic_number函数的两个版本中,模式匹配允许我们将名称与某个较大构造值的组件关联起来。然而,模式匹配的真正威力并不出现在这种简单的情况下,而是出现在要匹配的值的结构可能直到运行时才知道的情况下。例如,考虑一个返回二叉树节点的有序列表的函数:

In both versions of the atomic_number function, pattern matching allows us to associate names with the components of some larger constructed value. The real power of pattern matching, however, arises not in such simple cases, but in cases where the structure of the value to be matched may not be known until run time. Consider for example a function to return an in-order list of the nodes of a binary tree:

类型'a tree = Empty | 'a *'a tree *'a tree 的节点;;

type 'a tree = Empty | Node of 'a * 'a tree * 'a tree;;

让 rec 按顺序排列 t =

let rec inorder t =

 匹配 t

 match t with

 | 空 -> []

 | Empty -> []

 | 节点 (v,左,右) -> 中序左@[v]@中序右;;

 | Node (v, left, right) -> inorder left @ [v] @ inorder right;;

设计与实现

Design & Implementation

11.3 OCaml 中的类型等价

11.3 Type Equivalence in OCaml

由于使用类型推断,ML 系列语言通常提供结构类型等效的效果。如果需要,可以使用变体来获得名称等效的效果:

Because of their use of type inference, ML-family languages generally provide the effect of structural type equivalence. Variants can be used to obtain the effect of name equivalence when desired:

类型 celsius_temp = CT of int;;

type celsius_temp = CT of int;;

类型 fahrenheit_temp = FT of int;;

type fahrenheit_temp = FT of int;;

然后可以使用CT构造函数获取celsius_temp类型的值:

A value of type celsius_temp can then be obtained by using the CT constructor:

让冻结 = CT(0);;

let freezing = CT(0);;

不幸的是,celsius_temp不会自动继承int 的算术运算符和关系:表达式CT(0) + CT(20)将生成类型冲突错误消息。此外,除了内置比较运算符(<、>、<=、>=)外,OCaml 中没有提供重载功能:我们可以定义cplusfplus函数,但不能重载+本身。

Unfortunately, celsius_temp does not automatically inherit the arithmetic operators and relations of int: the expression CT(0) + CT(20) will generate a type clash error message. Moreover, with the exception of the built-in comparison operators (<, >, <=, >=), there is no provision for overloading in OCaml: we can define cplus and fplus functions, but we cannot overload + itself.

match构造将候选表达式(此处为t)与一系列模式(此处为 Empty 和 Node (v, left, right))中的每一个进行比较。每个模式前面都有一个竖线,并用箭头将其与伴随表达式分隔开。整体构造的值是与候选表达式匹配的第一个模式的伴随表达式的值。当应用于示例 11.39中的树时,我们的inorder函数得出['X'; 'R'; 'Z'; 'Y'; 'W']。■

The match construct compares a candidate expression (here t) with each of a series of patterns (here Empty and Node (v, left, right)). Each pattern is preceded by a vertical bar and separated by an arrow from an accompanying expression. The value of the overall construct is the value of the accompanying expression for the first pattern that matches the candidate expression. When applied to the tree of Example 11.39, our inorder function yields ['X'; 'R'; 'Z'; 'Y'; 'W']. ■

例 11.43

Example 11.43

守卫

Guards

在某些情况下,使用布尔表达式来保护模式可能会有所帮助。例如,假设我们在键值对列表中查找与给定键关联的值:

In some cases, it can be helpful to guard a pattern with a Boolean expression. Suppose, for example, that we are looking for the value associated with a given key in a list of key-value pairs:

让 rec 找到密钥 l =

let rec find key l =

 匹配 l

 match l with

 | [] -> 引发 Not_found

 | [] -> raise Not_found

 | (k, v) :: 当 k = key -> v 时休息

 | (k, v) :: rest when k = key -> v

 | 头部 :: rest -> 找到键 rest;;

 | head :: rest -> find key rest;;

让正方形=[(1,1); (2,4); (3,9); (4,16); (5,25)];;

let squares = [(1,1); (2,4); (3,9); (4,16); (5,25)];;

根据这些定义,找到 3 个方块将返回值9找到 6 个方块将引发Not_found异常。请注意,匹配中的模式是按程序顺序考虑的:在我们的find函数中,我们仅在第二个中的保护失败时才使用第三个替代方案。■

Given these definitions, find 3 squares will return the value 9; find 6 squares will raise a Not_found exception. Note that the patterns in a match are considered in program order: in our find function, we only use the third alternative when the guard in the second one fails. ■

例 11.44

Example 11.44

as关键字

The as keyword

如果需要,模式可以提供多个粒度级别的名称。例如,考虑将线段表示为一对表示端点坐标的对。给定一个线段s ,我们可以使用as关键字命名(两个分量)点和各个坐标:

When desired, a pattern can provide names at multiple levels of granularity. Consider, for example, the representation of a line segment as a pair of pairs indicating the coordinates of the endpoints. Given a segment s, we can name both the (two-component) points and the individual coordinates using the as keyword:

让 (((x1, y1) 作为 p1), ((x2, y2) 作为 p2)) = s;;

let (((x1, y1) as p1), ((x2, y2) as p2)) = s;;

如果s = ((1, 2), (3, 4)),那么经过此声明后,我们有x1 = 1, y1 = 2, x2 = 3, y2 = 4, p1 = (1, 2)p2 = (3, 4)。■

If s = ((1, 2), (3, 4)), then after this declaration, we have x1 = 1, y1 = 2, x2 = 3, y2 = 4, p1 = (1, 2),and p2 = (3, 4). ■

例 11.45

Example 11.45

function关键字

The function keyword

match的一个用例非常常见,因此值得拥有自己的语法糖。在我们的inorder函数中可以看到一个例子,其主体由函数(单个)参数上的match组成。特殊的函数关键字消除了明确命名参数的需要:

One use case for match is sufficiently common to warrant its own syntactic sugar. An example can be seen in our inorder function, whose body consists of a match on the function's (single) parameter. The special function keyword eliminates the need to name the parameter explicitly:

让 rec inorder = 函数

let rec inorder = function

 | 空 -> []

 | Empty -> []

 | 节点 (v,左,右) -> 中序左@[v]@中序右;;

 | Node (v, left, right) -> inorder left @ [v] @ inorder right;;

例 11.46

Example 11.46

运行时模式匹配

Run-time pattern matching

在许多情况下,OCaml 实现可以在编译时判断模式匹配会成功:它知道与模式匹配的值的结构的所有必要信息。在其他情况下,实现可以判断匹配注定会失败,通常是因为模式和值的类型无法统一。更有趣的情况是模式和值具有相同的类型(即可以统一),但匹配是否成功直到运行时才能确定。例如,如果l的类型为int list,则尝试将l “解构”为其头部和尾部可能会成功,也可能不会成功,具体取决于l的值:

In many cases, an OCaml implementation can tell at compile time that a pattern match will succeed: it knows all necessary information about the structure of the value being matched against the pattern. In other cases, the implementation can tell that a match is doomed to fail, generally because the types of the pattern and the value cannot be unified. The more interesting cases are those in which the pattern and the value have the same type (i.e., could be unified), but the success of the match cannot be determined until run time. If l is of type int list, for example, then an attempt to “deconstruct” l into its head and tail may or may not succeed, depending on l's value:

让 head :: rest = l 在…

let head :: rest = l in …

如果l[],则尝试匹配将引发Match_failure异常。■

If l is [], the attempted match will raise a Match_failure exception. ■

默认情况下,OCaml 编译器将对任何选项不详尽的模式匹配发出编译时警告— 即其结构未包含候选表达式类型所固有的所有可能性,因此其执行可能会导致运行时异常。如果多路匹配的后一分支中的模式完全被前一分支中的模式覆盖(意味着后者永远不会被选择),编译器也会发出警告。

By default, the OCaml compiler will issue a compile-time warning for any pattern match whose options are not exhaustive—i.e., whose structure does not include all the possibilities inherent in the type of the candidate expression, and whose execution might therefore lead to a run-time exception. The compiler will also issue a warning if the pattern in a later arm of a multi-way match is completely covered by one in an earlier arm (implying that the latter will never be chosen).

例 11.47

Example 11.47

图案覆盖范围

Coverage of patterns

完全覆盖的臂可能是一个错误,但无害,因为它永远不会导致动态语义错误。如果程序员可以预测模式在运行时始终有效,则非详尽的情况可能是故意的。示例 11.29append函数可以写成

A completely covered arm is probably an error, but harmless, in the sense that it will never result in a dynamic semantic error. Nonexhaustive cases may be intentional, if the programmer can predict that the pattern will always work at run time. The append function of Example 11.29 could have been written

让 rec 追加 11 12 =

let rec append 11 12 =

 如果 l1 = [] 那么 l2

 if l1 = [] then l2

 否则让 h::t = l1 在 h :: append t l2;;

 else let h::t = l1 in h :: append t l2;;

此版本的代码可能会引发警告:编译器无法意识到,只有当l1非空时, else子句中的let结构才会被详细说明。(此示例看起来很容易理解,但一般情况是不可判定的,提供特殊代码来识别简单情况没有什么意义。)编写代码的最佳方式可能是使用双向匹配,不是if…then…else

This version of the code is likely to elicit a warning: the compiler will fail to realize that the let construct in the else clause will be elaborated only if l1 is nonempty. (This example looks easy enough to figure out, but the general case is undecidable, and there is little point in providing special code to recognize easy cases.) Probably the best way to write the code is to use a two-way match instead of the if… then … else:

让 rec 附加 l1 l2 =

let rec append l1 l2 =

 将 l1 与

 match l1 with

 | [] -> l2

 | [] -> l2

 | h::t -> h ::附加 t l2;;

 | h::t -> h :: append t l2;;

与以前的版本不同,这允许编译器验证匹配是否详尽。■

Unlike either of the previous versions, this allows the compiler to verify that the matching is exhaustive. ■

例 11.48

Example 11.48

根据函数返回的元组进行模式匹配

Pattern matching against a tuple returned from a function

在命令式语言中,需要产生多个值的子程序通常通过修改引用或结果参数来实现。需要避免此类副作用的函数式语言必须安排从一个函数返回多个值。在 OCaml 中,这些值很容易组合成元组并从元组中提取。例如,考虑一个统计例程,它返回列表中值的平均值和标准差:

In imperative languages, subroutines that need to produce more than one value often do so via modification of reference or result parameters. Functional languages, which need to avoid such side effects, must instead arrange to return multiple values from a function. In OCaml, these are easily composed into, and extracted from, a tuple. Consider, for example, a statistics routine that returns the mean and standard deviation of the values in a list:

让统计量 l =

let stats l =

 让 rec 助手休息 n 总和 sum_squares =

 let rec helper rest n sum sum_squares =

  匹配其余

  match rest with

  | [] -> 让 nf = float_of_int n 在

  | [] -> let nf = float_of_int n in

     (总和 /.nf,sqrt(总和平方 /.nf))

     (sum /. nf, sqrt (sum_squares /. nf))

  | h :: t ->

  | h :: t ->

   辅助 t (n+1) (sum+.h) (sum_squares +. (h*.h)) 在

   helper t (n+1) (sum+.h) (sum_squares +. (h*.h)) in

 助手 l 0 0.0 0.0;;

 helper l 0 0.0 0.0;;

为了获取给定列表的统计信息,我们可以对函数stats返回的值进行模式匹配:

To obtain the statistics for a given list, we can pattern match against the value returned from function stats:

让(平均值,sd)=统计[1.;2.;3.;4.;5.];;

let (mean, sd) = stats [1.; 2.; 3.; 4.; 5.];;

解释者对此作出回应

To which the interpreter responds

val 平均值:浮点数 = 3。

val mean : float = 3.

val sd:浮点数 = 3.3166247903554

val sd : float = 3.3166247903554

11.4.5 控制流和副作用

11.4.5 Control Flow and Side Effects

例 11.49

Example 11.49

没有elseif

An if without an else

在前面几节中,我们已经看到了几个if表达式的例子。由于它必须产生一个值,几乎每个if表达式都既有then部分,又有else部分。唯一的例外是当then部分具有unit类型时,它会因为其副作用而执行:

We have seen several examples of if expressions in previous sections. Because it must yield a value, almost every if expression has both a then part and an else part. The only exception is when the then part has unit type, and is executed for its side effect:

如果 a < 0 则 print_string “negative”

if a < 0 then print_string “negative”

这里print_string调用的结果为(),按照惯例,隐式缺少else子句;因此整个表达式具有unit类型。■

Here the print_string call evaluates to (), as does, by convention, the implicit missing else clause; the overall expression thus has unit type. ■

I/O 是一种常见的副作用形式。OCaml 提供标准库例程来读取和打印各种内置类型。它还支持 C 的printf风格的格式化输出。

I/O is a common form of side effect. OCaml provides standard library routines to read and print a variety of built-in types. It also supports formatted output in the style of C's printf.

例 11.50

Example 11.50

OCaml 中的插入排序

Insertion sort in OCaml

虽然在表达重复执行时强烈推荐使用尾递归和高阶函数,但也可以使用迭代循环。它们最常见的应用可能是就地更新数组:

While tail recursion and higher-order functions are strongly preferred when expressing repeated execution, iterative loops are also available. Perhaps their most common application is to update arrays in place:

let insert_sort a =      (* 对数组 a 进行排序而不进行复制 *)

let insertion_sort a =     (* sort array a without making a copy *)

 对于 i = 1 到 Array.length a − 1 执行

 for i = 1 to Array.length a − 1 do

  令 t = a.(i) 在

  let t = a.(i) in

  让 j = ref i 在

  let j = ref i in

  while !j > 0 && t < a.(!j − 1) 执行

  while !j > 0 && t < a.(!j − 1) do

   a.(!j)<-a.(!j-1);

   a.(!j) <- a.(!j − 1);

   j := !j − 1

   j := !j − 1

  完毕;

  done;

  a.(!j) <- t

  a.(!j) <- t

 完毕;;

 done;;

请注意,这里既使用<-赋值来表示数组元素,又使用:=赋值来表示引用。(请记住,感叹号表示取消引用,而不是逻辑否定。)还请注意,使用分号来分隔 while循环内的赋值,并再次将while循环与对a.(!j) 的赋值分开。for和while循环都求值为( ) , <-:=赋值也是如此。■

Note the use here of both <- assignment for array elements and := assignment for references. (Keep in mind that the exclamation point indicates dereference, not logical negation.) Note also the use ofsemicolons to separate the assignments inside the while loop, and again to separate the while loop from the assignment to a.(!j). The for and while loops both evaluate to (), as do both <- and := assignments. ■

例 11.51

Example 11.51

一个简单的异常

A simple exception

我们在前面的章节中提到,OCaml 标准库中的许多例程在某些情况下会引发异常。我们在示例 11.43中自己引发了一个异常。一个简单的异常声明如下:

We have noted in previous sections that many routines in the OCaml standard library will raise exceptions in certain circumstances. We raised one ourselves in Example 11.43. A simple exception is declared as follows:

异常Not_found;;

exception Not_found;;

例 11.52

Example 11.52

带参数的异常

An exception with arguments

在更复杂的情况下,异常可以带有参数:

In more complex cases, an exception can take arguments:

浮点数 * 字符串的异常 Bad_arg;;

exception Bad_arg of float * string;;

后一种例外情况可能会由一个假设的三角函数库引发:

This latter exception might be raised by a hypothetical trigonometry library:

让 arc_cos x =

let arc_cos x =

 如果 x < −1. || x > 1. 则提高 (Bad_arg (x, “in arc_cos”))

 if x < −1. || x > 1. then raise (Bad_arg (x, “in arc_cos”))

 否则 acos x;;

 else acos x;;

当参数的幅度大于 1 时,预定义的acos函数仅返回一个“非数字”值(NaN — 部分 C-5.2.2)。■

The predefined acos function simply returns a “not-a-number” value (NaN—Section C-5.2.2) when its argument has a magnitude larger than 1. ■

例 11.53

Example 11.53

捕获异常

Catching an exception

异常被捕获在try表达式的with子句中:

Exceptions are caught in a with clause of a try expression:

让 special_meals =

let special_meals =

 [(“蒂姆·史密斯”,“素食主义者”); (“法蒂玛·侯赛因”,“清真”)];;

 [(“Tim Smith”, “vegan”); (“Fatima Hussain”, “halal”)];;

让 meal_type p =

let meal_type p =

 尝试使用 Not_found -> “default” 查找 p special_meals;;

 try find p special_meals with Not_found -> “default”;;

meal_type“蒂姆·史密斯”;;      ⇒ “素食”

meal_type “Tim Smith”;;     ⇒ “vegan”

meal_type“彭晨”;;      ⇒ “默认”

meal_type “Peng Chen”;;     ⇒ “default”

带有参数的异常仅稍微复杂一些:

An exception with an argument is only slightly more complicated:

open Printf;; (* 格式化 I/O 库 *)

open Printf;; (* formatted I/O library *)

让 c = 尝试用 Bad_arg (arg, loc) -> 进行 arc_cos v

let c = try arc_cos v with Bad_arg (arg, loc) ->

 (eprintf “错误参数 %f %s\n” arg loc; 0.0);;

 (eprintf “Bad argument %f %s\n” arg loc; 0.0);;

注意箭头后面的表达式必须与trywith之间的表达式具有相同的类型。这里我们打印了一条错误消息,然后(在分号后)提供了一个值 0。■

Note that the expression after the arrow must have the same type as the expression between the try and with. Here we have printed an error message and then (after the semicolon) provided a value of 0. ■

11.4.6 扩展示例:OCaml 中的 DFA 模拟

11.4.6 Extended Example: DFA Simulation in OCaml

例 11.54

Example 11.54

在 OCaml 中模拟 DFA

Simulating a DFA in OCaml

为了结束对 OCaml 的介绍,我们重新演绎了示例 11.20中 Scheme 中最初提出的 DFA 模拟程序。代码如图11.3所示。有限自动机的详细信息可以在第 2.2 节和 C-2.4.1 节中找到。在这里,我们将 DFA 表示为具有三个字段的记录:起始状态、转换函数和最终状态列表。为了表示转换函数,我们使用三元组列表。每个三元组的前两个元素是状态和输入符号。如果它们与当前状态和下一个输入符号匹配,则有限自动机进入由三元组的第三个元素给出的状态。

To conclude our introduction to OCaml, we reprise the DFA simulation program originally presented in Scheme in Example 11.20. The code appears in Figure 11.3. Finite automata details can be found in Sections 2.2 and C-2.4.1. Here we represent a DFA as a record with three fields: the start state, the transition function, and a list of final states. To represent the transition function we use a list of triples. The first two elements of each triple are a state and an input symbol. If these match the current state and next input symbol, then the finite automaton enters the state given by the third element of the triple.

f11-03-9780124104099
图 11.3 OCaml 程序模拟 DFA 的动作。给定i ,函数 move 搜索从起始状态到某个新状态s的标记为i 的转换。如果搜索失败,find会引发异常Not_found,该异常会传播出move;否则move返回一个具有相同转换函数和最终状态的新机器,但以s作为其“起始”状态。请注意,代码在输入符号的类型上是多态的。主函数mock封装了一个尾递归辅助函数,该函数累积一个移动的倒排列表,并在使用完所有输入符号后返回。然后,封装函数检查辅助函数是否以最终状态结束;它返回(正确排序的)一系列移动,以及接受拒绝指示。内置选项构造函数(例 7.6)用于区分真实状态(某些s)和错误状态()。

为了使这一切具体化,请考虑图 11.4中的 DFA 。它接受所有字母出现偶数次的ab字符串。为了模拟该机器,我们将它与输入字符串一起传递给函数模拟。在运行时,自动机将其经过的状态的踪迹累积为列表。一旦输入耗尽,它就会将踪迹与AcceptReject一起打包成一个元组。例如,如果我们输入

To make all this concrete, consider the DFA of Figure 11.4. It accepts all strings of as and bs in which each letter appears an even number of times. To simulate this machine, we pass it to the function simulate along with an input string. As it runs, the automaton accumulates as a list a trace of the states through which it has traveled. Once the input is exhausted, it packages the trace together in a tuple with either Accept or Reject. For example, if we type

f11-04-9780124104099
图 11.4 DFA 接受所有包含偶数个 的 as 和 bs 的字符串。图的底部是机器作为 OCaml 数据结构的表示,使用图 11.3的约定。

模拟 a_b_even_dfa ['a'; 'b'; 'b'; 'a'; 'b'];;

simulate a_b_even_dfa ['a'; 'b'; 'b'; 'a'; 'b'];;

然后 OCaml 解释器将会打印

then the OCaml interpreter will print

- :状态列表 * 决策 = ([0; 2; 3; 2; 0; 1], 拒绝)

- : state list * decision = ([0; 2; 3; 2; 0; 1], Reject)

如果我们将输入字符串更改为abaaba,解释器将打印

If we change the input string to abaaba, the interpreter will print

- :状态列表 * 决策 = ([0; 2; 3; 1; 3; 2; 0], 接受)

- : state list * decision = ([0; 2; 3; 1; 3; 2; 0], Accept)

11-01-9780124104099检查你的理解

Check Your Understanding

12. 为什么 OCaml 为整数和浮点值提供单独的算术运算符?

12. Why does OCaml provide separate arithmetic operators for integer and floating-point values?

13.解释 OCaml 中 物理值结构值相等之间的区别。

13. Explain the difference between physical and structural equality of values in OCaml.

14.  OCaml 中的列表与 Lisp 和 Scheme 中的列表有何不同?

14. How do lists in OCaml differ from those of Lisp and Scheme?

15.确定 OCaml 视为 可变的值。

15. Identify the values that OCaml treats as mutable.

16.列出 OCaml 执行 模式匹配的三种上下文。

16. List three contexts in which OCaml performs pattern matching.

17.解释一下 OCaml 中元组记录之间的区别。OCaml 记录与 C 或 Pascal 等语言中的记录(结构)有何不同?

17. Explain the difference between tuples and records in OCaml. How does an OCaml record differ from a record (structure) in languages like C or Pascal?

18. 什么是 OCaml变体?它们从 C 和 Pascal 等命令式语言中吸收了哪些特性?

18. What are OCaml variants? What features do they subsume from imperative languages such as C and Pascal?

11.5 重新审视评估顺序

11.5 Evaluation Order Revisited

6.6.2 节中,我们观察到许多表达式的子组件可以按多种顺序求值。具体来说,可以选择在将函数参数传递给函数之前对其进行求值,或者不求值就传递它们。前一种选择称为应用顺序求值;后者称为正常顺序求值。与大多数命令式语言一样,Scheme 和 OCaml 在大多数情况下使用应用顺序。在命令式语言的宏和按名称调用参数中出现的正常顺序在特殊情况下可用。

In Section 6.6.2 we observed that the subcomponents of many expressions can be evaluated in more than one order. In particular, one can choose to evaluate function arguments before passing them to a function, or to pass them unevaluated. The former option is called applicative-order evaluation; the latter is called normal-order evaluation. Like most imperative languages, Scheme and OCaml use applicative order in most cases. Normal order, which arises in the macros and call-by-name parameters of imperative languages, is available in special cases.

例 11.55

Example 11.55

应用和正常顺序评估

Applicative and normal-order evaluation

例如,假设我们在 Scheme 中定义了以下函数:

Suppose, for example, that we have defined the following function in Scheme:

(定义 double(lambda(x)(+ xx)))

(define double (lambda (x) (+ x x)))

按照应用顺序计算表达式(double (* 3 4)) (如 Scheme 所做的那样),我们得到

Evaluating the expression (double (* 3 4)) in applicative order (as Scheme does), we have

 (双精度(* 3 4))

 (double (* 3 4))

⇒(双12)

⇒ (double 12)

⇒ (+ 12 12)

⇒ (+ 12 12)

⇒ 24

⇒ 24

在正常顺序评估下,我们会有

Under normal-order evaluation we would have

 (双精度(* 3 4))

 (double (* 3 4))

⇒ (+ (* 3 4) (* 3 4))

⇒ (+ (* 3 4) (* 3 4))

⇒ (+ 12 (* 3 4))

⇒ (+ 12 (* 3 4))

⇒ (+ 12 12)

⇒ (+ 12 12)

⇒ 24

⇒ 24

这里我们最终做了额外的工作:正常顺序导致我们评估(* 3 4)两次。■

Here we end up doing extra work: normal order causes us to evaluate (* 3 4) twice. ■

例 11.56

Example 11.56

正常顺序避免不必要的工作

Normal-order avoidance of unnecessary work

在其他情况下,应用顺序求值可能会产生额外的工作。假设我们定义了以下内容:

In other cases, applicative-order evaluation can end up doing extra work. Suppose we have defined the following:

(定义开关

(define switch

 (λ(xabc)

 (lambda (x a b c)

  (条件((< x 0)a)

  (cond ((< x 0) a)

   ((= x 0) b)

   ((= x 0) b)

   ((> x 0)c))))

   ((> x 0) c))))

按应用顺序求表达式(switch −1 (+ 1 2) (+ 2 3) (+ 3 4)),可得

Evaluating the expression (switch −1 (+ 1 2) (+ 2 3) (+ 3 4)) in applicative order, we have

 (开关 −1 (+ 1 2) (+ 2 3) (+ 3 4))

 (switch −1 (+ 1 2) (+ 2 3) (+ 3 4))

⇒ (开关 −1 3 (+ 2 3 )(+ 3 4))

⇒ (switch −1 3 (+ 2 3) (+ 3 4))

⇒ (开关 −1 3 5(+3 4))

⇒ (switch −1 3 5 (+3 4))

⇒ (开关 −1 3 5 7)

⇒ (switch −1 3 5 7)

⇒ (条件((< −1 0)3)

⇒ (cond ((< −1 0) 3)

     ((= −1 0)5)

     ((= −1 0) 5)

     ((> −1 0)7)

     ((> −1 0) 7)

⇒ (条件(#t 3)

⇒ (cond (#t 3)

     ((= −1 0)5)

     ((= −1 0) 5)

     ((> −1 0)7)

     ((> −1 0) 7)

⇒ 3

⇒ 3

(这里我们假设cond是内置的,并且以惰性求值的方式执行其参数,尽管switch是急切地执行的。)在正常顺序求值下,我们会有

(Here we have assumed that cond is built in, and evaluates its arguments lazily, even though switch is doing so eagerly.) Under normal-order evaluation we would have

 (开关 −1 (+ 1 2) (+ 2 3) (+ 3 4))

 (switch −1 (+ 1 2) (+ 2 3) (+ 3 4))

⇒ (条件((< −1 0) (+ 1 2))

⇒ (cond ((< −1 0) (+ 1 2))

     ((= −1 0)(+ 2 3))

     ((= −1 0) (+ 2 3))

     ((> −1 0)(+ 3 4)))

     ((> −1 0) (+ 3 4)))

⇒ (条件 (#t (+ 1 2))

⇒ (cond (#t (+ 1 2))

     ((= −1 0)(+ 2 3))

     ((= −1 0) (+ 2 3))

     ((> −1 0)(+ 3 4)))

     ((> −1 0) (+ 3 4)))

⇒ (+ 1 2)

⇒ (+ 1 2)

⇒ 3

⇒ 3

这里,正常顺序求值避免了求值 ( + 2 3)( + 3 4)。(在这种情况下,我们假设算术和逻辑函数(如+<)是内置的,并强制求值它们的参数。)■

Here normal-order evaluation avoids evaluating (+ 2 3) or (+ 3 4). (In this case, we have assumed that arithmetic and logical functions such as + and < are built in, and force the evaluation of their arguments.) ■

在 Scheme 概述中,我们多次区分了特殊形式和函数。函数的参数始终通过共享传递(第 9.3.1 节),并在传递之前进行求值(即按应用顺序)。特殊形式的参数不经求值传递 — 换句话说,按名称传递。每个特殊形式都可以自由选择何时(以及是否)对其参数求值。例如, Cond以一系列未求值的对作为参数。它在内部一次一个地求值它们的car,当找到一个求值为#t 的car 时停止。

In our overview of Scheme we differentiated on several occasions between special forms and functions. Arguments to functions are always passed by sharing (Section 9.3.1), and are evaluated before they are passed (i.e., in applicative order). Arguments to special forms are passed unevaluated—in other words, by name. Each special form is free to choose internally when (and if) to evaluate its parameters. Cond, for example, takes a sequence of unevaluated pairs as arguments. It evaluates their cars internally, one at a time, stopping when it finds one that evaluates to #t.

在 Scheme 中,特殊形式和函数统称为表达式类型。一些表达式类型是原始的,因为它们必须内置于语言实现中。其他表达式类型是派生的;它们可以用原始表达式类型来定义。在基于eval/apply的解释器中,原始特殊形式内置于eval中;原始函数可由apply识别。我们已经了解了如何使用特殊形式lambda创建派生函数,这些函数可以用let绑定到名称。Scheme 提供了一种类似的特殊形式syntax-rules,可用于创建派生特殊形式。然后可以使用define-syntaxlet-syntax将它们绑定到名称。派生特殊形式在 Scheme 中称为,但与大多数其他宏不同,它们是卫生的— 词法作用域,集成到语言的语义中,并且不会出现第 3.7 节中描述的错误分组和变量捕获问题。与 C++ 模板(第 C-7.3.2 节)一样,Scheme 宏是图灵完备的。它们的行为类似于通过名称传递参数的函数(第 C-9.3.2 节),而不是通过共享传递参数。但它们是通过解释器的解析器和语义分析器中的逻辑扩展来实现的,而不是通过使用 thunk 的延迟求值来实现的。

Together, special forms and functions are known as expression types in Scheme. Some expression types are primitive, in the sense that they must be built into the language implementation. Others are derived; they can be defined in terms of primitive expression types. In an eval/apply-based interpreter, primitive special forms are built into eval; primitive functions are recognized by apply. We have seen how the special form lambda can be used to create derived functions, which can be bound to names with let. Scheme provides an analogous special form, syntax-rules, that can be used to create derived special forms. These can then be bound to names with define-syntax and let-syntax. Derived special forms are known as macros in Scheme, but unlike most other macros, they are hygienic—lexically scoped, integrated into the language's semantics, and immune from the problems of mistaken grouping and variable capture described in Section 3.7. Like C++ templates (Section C-7.3.2), Scheme macros are Turing complete. They behave like functions whose arguments are passed by name (Section C-9.3.2) instead of by sharing. They are implemented, however, via logical expansion in the interpreter's parser and semantic analyzer, rather than by delayed evaluation with thunks.

11.5.1 严格性和惰性求值

11.5.1 Strictness and Lazy Evaluation

求值顺序不仅会影响执行速度,还会影响程序的正确性。在应用顺序求值下遇到动态语义错误或“不需要的”子表达式中的无限回归的程序可能会在正常顺序求值下成功终止。如果一个(无副作用的)函数在其任何参数未定义时未定义(无法终止或遇到错误),则该函数被称为严格函数。这样的函数可以安全地求值其所有参数,因此其结果将不依赖于求值顺序。如果函数不施加此要求,即有时即使其参数之一未定义,该函数也定义,则该函数被称为非严格函数。如果一种语言定义方式使得函数始终是严格的,则该语言被称为严格语言。如果一种语言允许定义非严格函数,则该语言被称为非严格语言。如果一种语言始终按应用顺序求值表达式,则每个函数都保证是严格的,因为只要参数未定义,其求值就会失败,传递给该函数的函数也会失败。相反,非严格语言不能使用应用顺序;它必须使用正常顺序以避免评估不需要的参数。标准 ML、OCaml 和(宏除外)Scheme 是严格的。Miranda 和 Haskell 是非严格的。

Evaluation order can have an effect not only on execution speed but also on program correctness. A program that encounters a dynamic semantic error or an infinite regression in an “unneeded” subexpression under applicative-order evaluation may terminate successfully under normal-order evaluation. A (side-effect-free) function is said to be strict if it is undefined (fails to terminate, or encounters an error) when any of its arguments is undefined. Such a function can safely evaluate all its arguments, so its result will not depend on evaluation order. A function is said to be nonstrict if it does not impose this requirement—that is, if it is sometimes defined even when one of its arguments is not. A language is said to be strict if it is defined in such a way that functions are always strict. A language is said to be nonstrict if it permits the definition of nonstrict functions. If a language always evaluates expressions in applicative order, then every function is guaranteed to be strict, because whenever an argument is undefined, its evaluation will fail and so will the function to which it is being passed. Contrapositively, a nonstrict language cannot use applicative order; it must use normal order to avoid evaluating unneeded arguments. Standard ML, OCaml, and (with the exception of macros) Scheme are strict. Miranda and Haskell are nonstrict.

自动实现的惰性求值为我们提供了正常顺序求值的优势(不求值不需要的子表达式),同时在需要所有内容的表达式中,其运行速度是应用顺序求值速度的常数倍。诀窍是在内部为每个参数标记一个“备忘录”,以指示其值(如果已知)。任何试图求值参数的尝试都会将备忘录中的值设置为副作用,或者如果已经设置了值,则返回该值(无需重新计算)。

Lazy evaluation, implemented automatically, gives us the advantage of normal-order evaluation (not evaluating unneeded subexpressions) while running within a constant factor of the speed of applicative-order evaluation for expressions in which everything is needed. The trick is to tag every argument internally with a “memo” that indicates its value, if known. Any attempt to evaluate the argument sets the value in the memo as a side effect, or returns the value (without recalculating it) if it is already set.

例 11.57

Example 11.57

避免使用惰性求值

Avoiding work with lazy evaluation

回到示例 11.55中的表达式,(double (* 3 4))将在惰性系统中编译为(double (f)),其中f是具有内部副作用的隐藏闭包:

Returning to the expression of Example 11.55, (double (* 3 4)) willbe compiled in a lazy system as (double (f)), where f is a hidden closure with an internal side effect:

(定义 f

(define f

 (λ()

 (lambda ()

  (let ((done #f)      ; 备忘录最初未设置

  (let ((done #f)     ; memo initially unset

    (备忘录'())

    (memo '())

    (代码 (lambda () (* 3 4))))

    (code (lambda () (* 3 4))))

   (如果完成备忘录     ;如果设置了备忘录,则返回它

   (if done memo     ; if memo is set, return it

    (开始

    (begin

     (set! memo (code))      ;记住值

     (set! memo (code))     ; remember value

     (set!done #t)     ;注意我们设置了它

     (set! done #t)     ; note that we set it

     memo)))))      ; 并将其返回

     memo)))))     ; and return it

  (双(f))

  (double (f))

⇒ (+ (f) (f))

⇒ (+ (f) (f))

⇒ (+ 12 (f))      ;第一次调用计算值

⇒ (+ 12 (f))     ; first call computes value

⇒ (+ 12 12)      ;第二次调用返回记住的值

⇒ (+ 12 12)     ; second call returns remembered value

⇒ 24

⇒ 24

这里(* 3 4)仅被求值一次。虽然在这种情况下,处理备忘录的成本显然高于额外乘法的成本,但如果我们用非常昂贵的操作替换(* 3 4),那么节省的成本可能会相当可观。■

Here (* 3 4) will be evaluated only once. While the cost of manipulating memos will clearly be higher than that of the extra multiplication in this case, if we were to replace (* 3 4) with a very expensive operation, the savings could be substantial. ■

惰性求值对于“无限”数据结构特别有用,如第 6.6.2 节所述。它在只需要检查可能很长的列表的前缀的程序中也很有用(参见练习 11.10)。惰性求值用于 Miranda 和 Haskell 中的所有参数。在 Scheme 中,它通过显式使用延迟强制6以及在 OCaml 中通过标准Lazy库的类似机制。在 Scheme 中(在某些情况下)也可以通过使用宏来隐式实现。正常顺序求值可以认为是使用按名称调用参数的函数求值,而惰性求值有时被称为采用“按需调用”。除了 Miranda 和 Haskell 之外,在统计学家广泛使用的 R 脚本语言中也可以找到按需调用。

Lazy evaluation is particularly useful for “infinite” data structures, as described in Section 6.6.2. It can also be useful in programs that need to examine only a prefix of a potentially long list (see Exercise 11.10). Lazy evaluation is used for all arguments in Miranda and Haskell. It is available in Scheme through explicit use of delay and force,6 and in OCaml through the similar mechanisms of the standard Lazy library. It can also be achieved implicitly in Scheme (in certain contexts) through the use of macros. Where normal-order evaluation can be thought of as function evaluation using call-by-name parameters, lazy evaluation is sometimes said to employ “call by need.” In addition to Miranda and Haskell, call by need can be found in the R scripting language, widely used by statisticians.

设计与实现

Design & Implementation

11.4 惰性求值

11.4 Lazy evaluation

纯函数式语言的优点之一是,它使惰性求值成为一种完全透明的性能优化:程序员可以按照非严格函数和正常顺序求值来思考,依靠实现来避免重复求值的成本。然而,对于具有命令式特征的语言,这种特征并不成立:惰性求值在存在副作用的情况下是不透明的

One of the beauties of a purely functional language is that it makes lazy evaluation a completely transparent performance optimization: the programmer can think in terms of nonstrict functions and normal-order evaluation, counting on the implementation to avoid the cost of repeated evaluation. For languages with imperative features, however, this characterization does not hold: lazy evaluation is not transparent in the presence of side effects.

惰性求值的主要问题是其在存在副作用时的行为。如果参数包含对可通过赋值修改的变量的引用,则参数的值将取决于它是在赋值之前还是之后求值。同样,如果参数包含赋值,程序中其他地方的值可能取决于求值发生的时间。这些问题不会出现在 Miranda 或 Haskell 中,因为它们是纯函数式的:没有副作用。Scheme 和 OCaml 将问题留给程序员,但要求每次使用delay表达式时都将其括在force中,这样可以相对容易地识别出存在副作用的地方。

The principal problem with lazy evaluation is its behavior in the presence of side effects. If an argument contains a reference to a variable that may be modified by an assignment, then the value of the argument will depend on whether it is evaluated before or after the assignment. Likewise, if the argument contains an assignment, values elsewhere in the program may depend on when evaluation occurs. These problems do not arise in Miranda or Haskell because they are purely functional: there are no side effects. Scheme and OCaml leave the problem up to the programmer, but require that every use of a delay-ed expression be enclosed in force, making it relatively easy to identify the places where side effects are an issue.

11.5.2 I/O:流和 Monad

11.5.2 I/O: Streams and Monads

在传统 I/O 中可以发现一个主要的副作用来源:输入例程通常每次调用时都会返回不同的值;如果要认为程序正确,则对输出例程的多次调用必须按正确的顺序进行,尽管它们从不返回任何值。

A major source of side effects can be found in traditional I/O: an input routine will generally return a different value every time it is called, and multiple calls to an output routine, though they never return a value, must occur in the proper order if the program is to be considered correct.

例 11.58

Example 11.58

基于流的程序执行

Stream-based program execution

避免这些副作用的一种方法是将输入和输出建模为——无限长度的列表,其元素是惰性生成的。我们在6.6.2 节的无限列表中看到了一个流的示例(练习 11.18中有一个 OCaml 示例)。如果我们将输入和输出建模为流,则程序采用以下形式

One way to avoid these side effects is to model input and output as streams—unbounded-length lists whose elements are generated lazily. We saw an example of a stream in the infinite lists of Section 6.6.2 (an OCaml example appears in Exercise 11.18). If we model input and output as streams, then a program takes the form

(定义输出(my_prog 输入))

(define output (my_prog input))

当需要输入值时,函数my_prog会强制评估输入的car(头部),并将cdr(尾部)传递给程序的其余部分。为了推动执行,语言实现会反复强制评估输出car,将其打印出来,然后重复:

When it needs an input value, function my_prog forces evaluation of the car (head) of input, and passes the cdr (tail) on to the rest of the program. To drive execution, the language implementation repeatedly forces evaluation of the car of output, prints it, and repeats:

(定义驱动程序

(define driver

 (λ(s)

 (lambda (s)

  (if (null? s) '()      ; 没有剩余

  (if (null? s) '()     ; nothing left

   (开始

   (begin

    (显示(汽车))

    (display (car s))

    (驱动程序(cdr s))))))

    (driver (cdr s))))))

(驱动器输出)

(driver output)

例 11.59

Example 11.59

使用流进行交互式 I/O

Interactive I/O with streams

为了具体化,假设我们要编写一个纯函数式程序,提示用户输入一个数字序列(一次一个!)并打印它们的平方。如果 Scheme 采用了输入输出流的惰性求值(但它没有),那么我们可以这样写:

To make things concrete, suppose we want to write a purely functional program that prompts the user for a sequence of numbers (one at a time!) and prints their squares. If Scheme employed lazy evaluation of input and output streams (it doesn't), then we could write:

(定义正方形

(define squares

 (λ(s)

 (lambda (s)

  (缺点“请输入一个数字\n”

  (cons “please enter a number\n”

   (让((n(汽车 s)))

   (let ((n (car s)))

    (如果(eof 对象?n)'()

    (if (eof-object? n) '()

     (cons(* nn)(cons #\newline(squares(cdr s))))))))

     (cons (* n n) (cons #\newline (squares (cdr s)))))))))

(定义输出(平方输入)))

(define output (squares input)))

提示、输入和输出(即方块)会自然地在时间上交错。实际上,惰性求值会强制事情以正确的顺序发生:输出car是第一个提示。输出cadr(尾部的头部)是第一个方块,这个值需要对输入car进行求值。输出caddr(尾部的尾部的头部)是第二个提示。输出cadddr(尾部的尾部的尾部的头部)是第二个方块,这个值需要对输入cadr进行求值。■

Prompts, inputs, and outputs (i.e., squares) would be interleaved naturally in time. In effect, lazy evaluation would force things to happen in the proper order: The car of output is the first prompt. The cadr of output (the head of the tail) is the first square, a value that requires evaluation of the car of input. The caddr of output (the head of the tail of the tail) is the second prompt. The cadddr of output (the head of the tail of the tail of the tail) is the second square, a value that requires evaluation of the cadr of input. ■

流是 Haskell 早期版本中 I/O 系统的基础。不幸的是,虽然流成功地封装了终端交互的命令式本质,但它对于图形或随机访问文件来说效果并不好。它们还使得适应不同类型的 I/O 变得困难(因为 Haskell 中列表的所有元素都必须是单一类型)。Haskell 的较新版本使用了一个更通用的概念,称为monads。monad 源自数学的一个分支,称为范畴论,但人们不需要理解该理论就可以体会到它们在实践中的用处。在 Haskell 中,monad 本质上是高阶函数的巧妙使用,再加上一些语法糖,允许程序员将一系列必须按顺序发生的动作(函数调用)链接在一起。这个想法的强大之处在于能够将任意复杂度的隐藏结构化值从一个动作传递到下一个动作。在 monad 的许多应用中,这个额外的隐藏值扮演着可变状态的角色:携带到连续动作的值之间的差异充当了副作用。

Streams formed the basis of the I/O system in early versions of Haskell. Unfortunately, while they successfully encapsulate the imperative nature of interaction at a terminal, streams don't work very well for graphics or random access to files. They also make it difficult to accommodate I/O of different kinds (since all elements of a list in Haskell must be of a single type). More recent versions of Haskell employ a more general concept known as monads. Monads are drawn from a branch of mathematics known as category theory, but one doesn't need to understand the theory to appreciate their usefulness in practice. In Haskell, monads are essentially a clever use of higher-order functions, coupled with a bit of syntactic sugar, that allow the programmer to chain together a sequence of actions (function calls) that have to happen in order. The power of the idea comes from the ability to carry a hidden, structured value of arbitrary complexity from one action to the next. In many applications of monads, this extra hidden value plays the role of mutable state: differences between the values carried to successive actions act as side effects.

例 11.60

Example 11.60

Haskell 中的伪随机数

Pseudorandom numbers in Haskell

作为一个比 I/O 稍微简单一些的激励示例,考虑按照示例 6.45的思路创建伪随机数生成器 (RNG) 的可能性。在该示例中,我们假设rand()会作为副作用修改隐藏状态,允许它在每次调用时返回不同的值。这种用法在纯函数式语言中是不可能的,但我们可以通过将状态传递给函数并让其与随机数一起返回新状态来获得类似的效果。这正是 Haskell 中内置函数random 的工作方式。以下代码调用random两次来说明其接口。

As a motivating example somewhat simpler than I/O, consider the possibility of creating a pseudorandom number generator (RNG) along the lines of Example 6.45. In that example we assumed that rand() would modify hidden state as a side effect, allowing it to return a different value every time it is called. This idiom isn't possible in a pure functional language, but we can obtain a similar effect by passing the state to the function and having it return new state along with the random number. This is exactly how the built-in function random works in Haskell. The following code calls random twice to illustrate its interface.

twoRandomlnts :: StdGen -> ([Integer],StdGen)

twoRandomlnts :: StdGen -> ([Integer], StdGen)

 --类型签名:twoRandomlnts 是一个接受

 --type signature: twoRandomlnts is a function that takes an

 --StdGen(RNG 的状态)并返回一个包含

 --StdGen (the state of the RNG) and returns a tuple containing

 ——一个整数列表和一个新的 StdGen。

 --a list of Integers and a new StdGen.

twoRandomInts gen = let

twoRandomInts gen = let

  (randl, gen2) = 随机数生成器

  (randl, gen2) = random gen

  (rand2, gen3) = 随机 gen2

  (rand2, gen3) = random gen2

 在([randl,rand2],gen3)中

 in ([randl, rand2], gen3)

主要=让

main = let

  gen = mkStdGen 123       – 新的 RNG,以 123 为种子

  gen = mkStdGen 123      -- new RNG, seeded with 123

  ints = fst (twoRandomlnts gen)       -- 提取第一个元素

  ints = fst (twoRandomlnts gen)      -- extract first element

 打印 ints           -- 返回元组

 in print ints          -- of returned tuple

请注意,gen2是第一次调用random的返回值之一,已作为参数传递给第二次调用。然后,gen3是第二次调用的返回值之一,返回到main,如果我们愿意,它可以在那里传递给另一个函数。这种机制有效,但远非完美:RNG 状态的副本必须“穿过”每个需要随机数的函数。对于深度嵌套的函数来说,这尤其复杂。很容易犯错,而且很难验证自己没有犯错。

Note that gen2, one of the return values from the first call to random, has been passed as an argument to the second call. Then gen3, one of the return values from the second call, is returned to main, where it could, if we wished, be passed to another function. This mechanism works, but it's far from pretty: copies of the RNG state must be “threaded through” every function that needs a random number. This is particularly complicated for deeply nested functions. It is easy to make a mistake, and difficult to verify that one has not.

Monads 为通过函数式程序线程化可变状态的问题提供了更通用的解决方案。下面是我们重写的示例,使用 Haskell 的标准IO monad,其中包括一个随机数生成器:

Monads provide a more general solution to the problem of threading mutable state through a functional program. Here is our example rewritten to use Haskell's standard IO monad, which includes a random number generator:

twoMoreRandomlnts :: IO [整数]

twoMoreRandomlnts :: IO [Integer]

 --twoMoreRandomInts 返回整数列表。它还

 --twoMoreRandomInts returns a list of Integers. It also

 ——隐式接受并返回 IO monad 的所有状态。

 --implicitly accepts, and returns, all the state of the IO monad.

twoMoreRandomInts = do

twoMoreRandomInts = do

 rand1 <- randomIO

 rand1 <- randomIO

 rand2 <- randomIO

 rand2 <- randomIO

 返回 [rand1, rand2]

 return [rand1, rand2]

主要 = 执行

main = do

 更多Ints <- twoMoreRandomInts

 moreInts <- twoMoreRandomInts

 打印更多Ints

 print moreInts

这里有几个不同之处。首先,twoMoreRandomInts函数的类型变成了IO [Integer]。这将其标识为IO 操作— 一个函数,除了返回显式整数列表之外,它还以不可见的方式接受并返回 IO monad (包括标准 RNG)的状态。类似地, randomIO的类型是IO Integer。为了将IO状态从一个操作串联到下一个操作, twoMoreRandomIntsmain 的主体使用do符号而不是let。do将一系列操作打包成一个复合操作。在此过程中的每一步,它都会将 monad 的(可能已修改的)状态从一个操作传递到下一个操作。它还支持“赋值”运算符<-,它将显式返回值与隐藏状态分开,并打开一个其左侧的嵌套范围,因此序列中较早“分配”的所有值对于序列中后续的操作都是可见的。

There are several differences here. First, the type of the twoMoreRandomInts function has become IO [Integer]. This identifies it as an IO action—a function that (in addition to returning an explicit list of integers) invisibly accepts and returns the state of the IO monad (including the standard RNG). Similarly, the type of randomIO is IO Integer. To thread the IO state from one action to the next, the bodies of twoMoreRandomInts and main use do notation rather than let. A do block packages a sequence of actions together into a single, compound action. At each step along the way, it passes the (potentially modified) state of the monad from one action to the next. It also supports the “assignment” operator, <-, which separates the explicit return value from the hidden state and opens a nested scope for its left-hand side, so all values “assigned” earlier in the sequence are visible to actions later in the sequence.

twoMoreRandomInts中的return运算符将显式返回值(在我们的例子中是两个元素的列表)与隐藏状态打包在一起,返回给调用者。randomIO中可能也出现了类似的return用法。我们所做的一切都是纯粹的功能性的 — do<-只是语法糖 — 但将 RNG 的状态从一次random调用传递到下一次random调用所需的记录已被隐藏,从而使我们的代码看起来是命令式的。■

The return operator in twoMoreRandomInts packages an explicit return value (in our case, a two-element list) together with the hidden state, to be returned to the caller. A similar use of return presumably appears inside randomIO. Everything we have done is purely functional—do and <- are simply syntactic sugar—but the bookkeeping required to pass the state of the RNG from one invocation of random to the next has been hidden in a way that makes our code look imperative. ■

例 11.61

Example 11.61

IO monad的状态

The state of the IO monad

那么这与 I/O 有什么关系呢?考虑getChar函数,它从标准输入读取一个字符。与rand一样,我们期望它每次调用时都返回不同的值。因此,Haskell 将getChar设置为IO Char类型:它返回一个字符,但也接受并传递 monad 的隐藏状态。

So what does this have to do with I/O? Consider the getChar function, which reads a character from standard input. Like rand, we expect it to return a different value every time we call it. Haskell therefore arranges for getChar to be of type IO Char: it returns a character, but also accepts, and passes on, the hidden state of the monad.

在大多数 Haskell monad 中,可以显式提取和检查隐藏状态。然而, IO monad 是抽象的:只有其状态的一部分在库头文件中定义;其余部分由语言运行时系统实现。这是不可避免的,因为实际上,IO monad的隐藏状态 包含现实世界。如果这种状态可见,程序可以捕获和重用它,荒谬的期望是我们可以“回到过去”并查看用户上周二对不同提示的响应方式。不幸的是,IO状态隐藏意味着IO T类型的值被永久污染:它永远不能从 monad 中提取以产生“纯 T ”。■

In most Haskell monads, hidden state can be explicitly extracted and examined. The IO monad, however, is abstract: only part of its state is defined in library header files; the rest is implemented by the language run-time system. This is unavoidable because, in effect, the hidden state of the IO monad encompasses the real world. If this state were visible, a program could capture and reuse it, with the nonsensical expectation that we could “go back in time” and see what the user would have done in response to a different prompt last Tuesday. Unfortunately, IO state hiding means that a value of type IO T is permanently tainted: it can never be extracted from the monad to produce a “pure T.” ■

例 11.62

Example 11.62

动作的功能组合

Functional composition of actions

因为IO操作只是普通值,所以我们可以像操作其他数据类型的值一样操作它们。最基本的输出操作是putChar,类型为Char -> IO ()(具有显式字符参数且没有显式返回的单子函数)。给定putChar,我们可以定义putStr

Because IO actions are just ordinary values, we can manipulate them in the same way as values of other data types. The most basic output action is putChar, of type Char -> IO () (monadic function with an explicit character argument and no explicit return). Given putChar, we can define putStr:

putStr :: String -> IO()

putStr :: String -> IO ()

putStr s =sequence_(map putChar s)

putStr s = sequence_ (map putChar s)

Haskell 中的字符串只是字符列表。map函数以函数f和列表l作为参数,并返回一个列表,该列表包含将f应用于l元素的结果:

Strings in Haskell are simply lists of characters. The map function takes a function f and a list l as argument, and returns a list that contains the results of applying f to the elements of l:

映射 :: (a->b) -> [a] -> [b]
映射 f [] = []-- 基本情况
地图 f (h:t) = fh: 地图 ft-- 尾部递归情况
-- ':' 类似于 Scheme 中的 cons

map putChar s的结果是一个操作列表,每个操作都会打印一个字符:其类型为[IO ()]。内置函数sequence_将其转换为打印列表的单个操作。它可以定义如下。

The result of map putChar s is a list of actions, each of which prints a character: it has type [IO ()]. The built-in function sequence_ converts this to a single action that prints a list. It could be defined as follows.

序列_ :: [IO()] -> IO()
序列_[] = 返回()-- 基本情况
序列_(a:more)= 执行a;序列_更多-- 尾部递归情况

与之前一样,do提供了一种将操作串联在一起的便捷方法。为简洁起见,我们将操作写在一行上,并用分号分隔。■

As before, do provides a convenient way to chain actions together. For brevity, we have written the actions on a single line, separated by a semicolon. ■

例 11.63

Example 11.63

流和 I/O monad

Streams and the I/O monad

Haskell 程序的入口点始终是函数main。它具有类型IO ()。由于 Haskell 是惰性的(非严格),因此main返回的操作序列仍然是假设的,直到运行时系统强制对其进行评估。实际上,Haskell 程序往往有一个小型的IO monad 代码顶层结构,用于对 I/O 操作进行排序。程序的大部分(值的计算和 I/O 操作发生顺序的确定)都是纯函数式的。对于可以用流来表示 I/O 的程序,顶层结构可能由一行组成:

The entry point of a Haskell program is always the function main. It has type IO (). Because Haskell is lazy (nonstrict), the action sequence returned by main remains hypothetical until the run-time system forces its evaluation. In practice, Haskell programs tend to have a small top-level structure of IO monad code that sequences I/O operations. The bulk of the program—both the computation of values and the determination of the order in which I/O actions should occur—is then purely functional. For a program whose I/O can be expressed in terms of streams, the top-level structure may consist of a single line:

main = 与 my_program 交互

main = interact my_program

库函数interact isoftype (String -> String) -> IO ()。它将一个从字符串到字符串的函数(在本例中为my_program)作为参数。它调用此函数,将标准输入的内容作为参数传递,并将结果写入标准输出。在内部,interact使用函数getContents,该函数将程序的输入作为延迟评估的字符串返回:一个流。在更复杂的程序中,main可能会协调更复杂的 I/O 操作,包括图形和对文件的随机访问。■

The library function interact isoftype (String -> String) -> IO (). Ittakes as argument a function from strings to strings (in this case my_program). It calls this function, passing the contents of standard input as argument, and writes the result to standard output. Internally, interact uses the function getContents, which returns the program's input as a lazily evaluated string: a stream. In a more sophisticated program, main may orchestrate much more complex I/O actions, including graphics and random access to files. ■

设计与实现

Design & Implementation

11.5 单子

11.5 Monads

Monad 在 Haskell 中被广泛使用。IO Monad 是命令式语言特性的中央存储库 — 不仅包括 I/O 和随机数,还包括可变全局变量共享内存同步。其他 Monad(具有可访问的隐藏状态)支持部分函数和各种容器类(列表和集合)。与惰性求值结合使用时,Monad 容器又为回溯搜索、非确定性和迭代器的功能等效性提供了自然基础。(例如,在列表 Monad 中,隐藏状态可以承载生成无限列表尾部所需的延续。)

Monads are very heavily used in Haskell. The IO monad serves as the central repository for imperative language features—not only I/O and random numbers but also mutable global variables and shared-memory synchronization. Additional monads (with accessible hidden state) support partial functions and various container classes (lists and sets). When coupled with lazy evaluation, monadic containers in turn provide a natural foundation for backtracking search, nondeterminism, and the functional equivalent of iterators. (In the list monad, for example, hidden state can carry the continuation needed to generate the tail of an infinite list.)

无法从IO monad 中提取值反映了物理世界是命令式的这一事实,需要以非平凡方式与物理世界交互的语言必须包含命令式特性。换句话说,IO monad(与一般的 monad 不同)不仅仅是语法糖:通过隐藏物理世界的状态,只要我们愿意强制执行顺序求值顺序,就可以表达原本无法以函数方式表达的事物。monad 的美妙之处在于它们将顺序性限制在典型程序的相对较小的一部分,因此副作用不会干扰大部分计算。

The inability to extract values from the IO monad reflects the fact that the physical world is imperative, and that a language that needs to interact with the physical world in nontrivial ways must include imperative features. Put another way, the IO monad (unlike monads in general) is more than syntactic sugar: by hiding the state of the physical world it makes it possible to express things that could not otherwise be expressed in a functional way, provided that we are willing to enforce a sequential evaluation order. The beauty of monads is that they confine sequentiality to a relatively small fraction of the typical program, so that side effects cannot interfere with the bulk of the computation.

11.6 高阶函数

11.6 Higher-Order Functions

例 11.64

Example 11.64

Scheme 中的map函数

map function in Scheme

如果一个函数接受一个函数作为参数,或者返回一个函数作为结果,则该函数被称为高阶函数(也称为函数形式)。我们已经在 Scheme 中看到了几个高阶函数的例子: call/cc第 6.2.2 节)、for-each示例 11.18)、compose示例 11.19)和apply第 11.3.5 节)。我们还在第 11.5.2 节中看到了高阶函数map的 Haskell 版本。Scheme 版本的map稍微更通用一些。与for-each一样,它接受一个函数和一系列列表作为参数。列表的数量必须与函数接受的参数一样多,并且所有列表的长度必须相同。Map在列表中相应的元素集上调用其函数参数:

A function is said to be a higher-order function (also called a functional form) if it takes a function as an argument, or returns a function as a result. We have seen several examples already of higher-order functions in Scheme: call/cc (Section 6.2.2), for-each (Example 11.18), compose (Example 11.19), and apply (Section 11.3.5). We also saw a Haskell version of the higher-order function map in Section 11.5.2. The Scheme version of map is slightly more general. Like for-each, it takes as argument a function and a sequence of lists. There must be as many lists as the function takes arguments, and the lists must all be of the same length. Map calls its function argument on corresponding sets of elements from the lists:

(地图 * '(2 4 6) '(3 5 7)) ⇒ (6 20 42)

(map * '(2 4 6) '(3 5 7)) ⇒ (6 20 42)

for -each 的执行是为了产生副作用,并且具有依赖于实现的返回值,而map则是纯函数式的:它返回由其函数参数返回的值组成的列表。■

Where for-each is executed for its side effects, and has an implementation-dependent return value, map is purely functional: it returns a list composed of the values returned by its function argument. ■

例 11.65

Example 11.65

Scheme 中的折叠(缩减)

Folding (reduction) in Scheme

Scheme(或 OCaml、Haskell 或其他函数式语言)的程序员可以轻松定义其他高阶函数。例如,假设我们希望能够使用关联二元运算符将列表的元素“折叠”在一起:

Programmers in Scheme (or in OCaml, Haskell, or other functional languages) can easily define other higher-order functions. Suppose, for example, that we want to be able to “fold” the elements of a list together, using an associative binary operator:

(定义折叠

(define fold

 (λ(fil)

 (lambda (f i l)

  (if (null? l) i      ; i 通常是 f 的恒等元素

  (if (null? l) i     ; i is commonly the identity element for f

   (f (car l) (fold fi (cdr l))))))

   (f (car l) (fold f i (cdr l))))))

现在(fold + 0 '(1 2 3 4 5))给出前五个自然数的和,而(fold * 1 '(1 2 3 4 5))给出它们的乘积。■

Now (fold + 0 '(1 2 3 4 5)) gives us the sum of the first five natural numbers, and (fold * 1 '(1 2 3 4 5)) gives us their product. ■

例 11.66

Example 11.66

OCaml 中的折叠

Folding in OCaml

OCaml 的List模块定义了类似的fold_left函数:

A similar fold_left function is defined by OCaml's List module:

fold_left (+) 0 [1; 2; 3; 4; 5];;⇒ 15
fold_left (*) 1 [1; 2; 3; 4; 5];;⇒ 120

( *周围的空格是必需的,以将其与注释分隔符区分开。)对于非结合运算符,类似的fold_right函数会从右到左折叠列表。但是,它不是尾递归的,并且不太常用。■

(The spaces around * are required to distinguish it from a comment delimiter.) For non associative operators, an analogous fold_right function folds the list from right-to-left. It is not tail-recursive, however, and tends to be used less often. ■

例 11.67

Example 11.67

组合高阶函数

Combining higher-order functions

高阶函数最常见的用途之一是从现有函数构建新函数:

One of the most common uses of higher-order functions is to build new functions from existing ones:

(定义总计(lambda(l)(折叠 + 0 l)))

(define total (lambda (l) (fold + 0 l)))

(总计'(1 2 3 4 5))          ⇒ 15

(total '(1 2 3 4 5))          ⇒ 15

(定义总计(lambda(l)(映射总计l)))

(define total-all (lambda (l) (map total l)))

(总计-全部'((1 2 3 4 5)

(total-all '((1 2 3 4 5)

          (2 4 6 8 10)

          (2 4 6 8 10)

          (3 6 9 12 15)))           ⇒ (15 30 45)

          (3 6 9 12 15)))          ⇒ (15 30 45)

(定义 make-double(lambda(f)(lambda(x)(fxx))))

(define make-double (lambda (f) (lambda (x) (f x x))))

(定义两次(make-double +))

(define twice (make-double +))

(定义正方形(make-double *))

(define square (make-double *))

柯里化

Currying

例 11.68

Example 11.68

柯里化的部分应用

Partial application with currying

一种以逻辑学家 Haskell Curry 命名的常见操作是将多参数函数替换为一个接受单个参数并返回需要其余参数的函数:

A common operation, named for logician Haskell Curry, is to replace a multiargument function with a function that takes a single argument and returns a function that expects the remaining arguments:

(定义 curried-plus(lambda(a)(lambda(b)(+ ab))))

(define curried-plus (lambda (a) (lambda (b) (+ a b))))

((柯里化-plus 3) 4)      ⇒ 7

((curried-plus 3) 4)     ⇒ 7

(定义 plus-3(curried-plus 3))

(define plus-3 (curried-plus 3))

(加 3 4)           ⇒ 7

(plus-3 4)          ⇒ 7

除此之外,柯里化还使我们能够将“部分应用”函数传递给高阶函数:

Among other things, currying gives us the ability to pass a “partially applied” function to a higher-order function:

(map (curried-plus 3) '(1 2 3))      ⇒ (4 5 6)

(map (curried-plus 3) '(1 2 3))     ⇒ (4 5 6)

例 11.69

Example 11.69

通用柯里化函数

General-purpose curry function

事实证明,我们可以在 Scheme 中编写一个通用函数来“科里化”​​其(二进制)函数参数:

It turns out that we can write a general-purpose function in Scheme that “curries” its (binary) function argument:

(定义 curry(lambda(f)(lambda(a)(lambda(b)(fab)))))

(define curry (lambda (f) (lambda (a) (lambda (b) (f a b)))))

(((库里+)3)4)           ⇒ 7

(((curry +) 3) 4)          ⇒ 7

(定义 curried-plus(curry +))

(define curried-plus (curry +))

设计与实现

Design & Implementation

11.6 高阶函数

11.6 Higher-order functions

如果高阶函数如此强大和有用,为什么它们在命令式编程语言中并不常见?似乎至少有两个重要的答案。首先,一等函数的大部分功能取决于动态创建新函数的能力,为此我们需要一个函数构造函数- 类似于 Scheme 的lambda或 OCaml 的fun。尽管函数构造函数出现在几种最近的语言中,但它们与传统命令式语言的语法和语义有着显著的不同。其次,将函数指定为返回值或将其存储在变量中(如果语言具有副作用)的能力要求我们要么消除函数嵌套(这会再次削弱程序动态创建具有所需行为的函数的能力),要么给予局部变量无限的范围,从而增加存储管理的成本。

If higher-order functions are so powerful and useful, why aren't they more common in imperative programming languages? There would appear to be at least two important answers. First, much of the power of first-class functions depends on the ability to create new functions on the fly, and for that we need a function constructor—something like Scheme's lambda or OCaml's fun. Though they appear in several recent languages, function constructors are a significant departure from the syntax and semantics of traditional imperative languages. Second, the ability to specify functions as return values, or to store them in variables (if the language has side effects), requires either that we eliminate function nesting (something that would again erode the ability of programs to create functions with desired behaviors on the fly) or that we give local variables unlimited extent, thereby increasing the cost of storage management.

例 11.70

Example 11.70

元组作为 OCaml 函数参数

Tuples as OCaml function arguments

ML 及其后代使得定义柯里化函数变得特别容易——这一点我们在11.4 节中略过了。考虑 OCaml 中的以下函数:

ML and its descendants make it especially easy to define curried functions—a fact that we glossed over in Section 11.4. Consider the following function in OCaml:

# 让 plus (a, b) = a + b;;

# let plus (a, b) = a + b;;

val plus : int * int -> int = <fun>

val plus : int * int -> int = <fun>

这里的第一行,我们以#提示符开头,是由用户输入的。第二行由 OCaml 解释器打印,并指出plus的推断类型。尽管人们可能将plus视为两个参数的函数,但 OCaml 定义指出所有函数都接受一个参数。我们声明的是一个以两个元素的元组作为参数的函数。要调用plus,我们将其名称和作为其参数的元组并列:

The first line here, which we have shown beginning with a # prompt, is entered by the user. The second line is printed by the OCaml interpreter, and indicates the inferred type of plus. Though one may think of plus as a function of two arguments, the OCaml definition says that all functions take a single argument. What we have declared is a function that takes a two-element tuple as argument. To call plus, we juxtapose its name and the tuple that is its argument:

#加法(3,4);;

# plus (3, 4);;

- :int = 7

- : int = 7

这里的括号不是函数调用语法的一部分;它们界定了元组(3, 4)。■

The parentheses here are not part of the function call syntax; they delimit the tuple (3, 4). ■

例 11.71

Example 11.71

单例参数上的可选括号

Optional parentheses on singleton arguments

我们可以声明一个单参数函数,而不用括号括起它的形式参数:

We can declare a single-argument function without parenthesizing its formal argument:

# 让两次 n = n + n;;

# let twice n = n + n;;

val 两次 = fn : int -> int

val twice = fn : int -> int

# 两次 2;;

# twice 2;;

- :int = 4

- : int = 4

如果需要,我们可以在声明或调用中添加括号,但由于里面没有逗号,所以不隐含元组:

We can add parentheses in either the declaration or the call if we want, but because there is no comma inside, no tuple is implied:

# 让 double (n) = n + n;;

# let double (n) = n + n;;

val double : int -> int = <fun>

val double : int -> int = <fun>

#两次(2);;

# twice (2);;

- :int = 4

- : int = 4

# 两次 2;;

# twice 2;;

- :int = 4

- : int = 4

#双精度(2);;

# double (2);;

- :int = 4

- : int = 4

# 双2;;

# double 2;;

- :int = 4

- : int = 4

在 OCaml 中,任何表达式周围都可以放置普通括号。■

Ordinary parentheses can be placed around any expression in OCaml. ■

例 11.72

Example 11.72

OCaml 中的简单柯里化函数

Simple curried function in OCaml

现在考虑一下柯里化函数的定义:

Now consider the definition of a curried function:

# 让 curried_plus a = fun b -> a + b;;

# let curried_plus a = fun b -> a + b;;

val curried_plus: int -> int -> int = <fun>

val curried_plus : int -> int -> int = <fun>

这里curried_plus的类型与示例 11.23中的内置+相同- 即int -> int -> int。这隐式分组为int -> (int -> int)。其中plus是将一对(元组)整数映射到整数的函数,而curried_plus是将整数映射到将整数映射到整数的函数的函数:

Here the type of curried_plus is the same as that of the built-in + in Example 11.23—namely int -> int -> int. This groups implicitly as int -> (int -> int). Where plus is a function mapping a pair (tuple) of integers to an integer, curried_plus is a function mapping an integer to a function that maps an integer to an integer:

# curried_plus 3;;

# curried_plus 3;;

- :int -> int = <fun>

- : int -> int = <fun>

# 加3 ;;

# plus 3;;

错误:此表达式的类型为 int,但预期的表达式类型为 int * int

Error: This expression has type int but an expression was expected of type int * int

例 11.73

Example 11.73

柯里化的简写形式

Shorthand notation for currying

为了更容易地声明诸如curried_plus 之类的函数,ML 系列语言(包括 OCaml)允许在函数声明的形式参数位置上使用一系列操作数:

To make it easier to declare functions like curried_plus, ML-family languages, OCaml among them, allow a sequence of operands in the formal parameter position of a function declaration:

# 让 curried_plus ab = a + b;;

# let curried_plus a b = a + b;;

val curried_plus: int -> int -> int = <fun>

val curried_plus : int -> int -> int = <fun>

此形式只是上例中声明的简写;它没有声明一个具有两个参数的函数。Curried_plus只有一个形式参数a。它的返回值是一个具有形式参数b的函数,该函数又返回a + b。■

This form is simply shorthand for the declaration in the previous example; it does not declare a function of two arguments. Curried_plus has a single formal parameter, a. Its return value is a function with formal parameter b that in turn returns a + b. ■

例 11.74

Example 11.74

在 OCaml 中构建fold_left

Building fold_left in OCaml

使用元组表示法,一个简单的、非柯里化的折叠函数可以在 OCaml 中声明如下:

Using tuple notation, a naive, non-curried fold function might be declared as follows in OCaml:

# 让 rec 折叠 (f, i, l) =

# let rec fold (f, i, l) =

 匹配 l

 match l with

 | [] -> 我

 | [] -> i

 | h :: t -> 折叠(f,f(i,h),t);;

 | h :: t -> fold (f, f (i, h), t);;

val fold : ('a * 'b -> 'b) * 'b * 'a 列表 -> 'b = <fun>

val fold : ('a * 'b -> 'b) * 'b * 'a list -> 'b = <fun>

柯里化版本可以声明如下:

A curried version might be declared as follows:

# 让 rec curried_fold fil =

# let rec curried_fold f i l =

 匹配 l

 match l with

 | [] -> 我

 | [] -> i

 | h :: t -> curried_fold f (f (i,h)) t;;

 | h :: t -> curried_fold f (f (i, h)) t;;

val curried_fold : ('a * 'b -> 'a) -> 'a -> 'b 列表 -> 'a = <fun>

val curried_fold : ('a * 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>

注意函数推断类型的差异。柯里化版本的优点是它能够接受部分参数列表:

Note the difference in the inferred types of the functions. The advantage of the curried version is its ability to accept a partial list of arguments:

# curried_fold plus;;

# curried_fold plus;;

- :int -> int 列表 -> int = <fun>

- : int -> int list -> int = <fun>

# curried_fold 加 0;;

# curried_fold plus 0;;

- :int 列表 -> int = <fun>

- : int list -> int = <fun>

# curried_fold 加 0 [1; 2; 3; 4; 5];;

# curried_fold plus 0 [1; 2; 3; 4; 5];;

- :int = 15

- : int = 15

为了获得内置fold_left的行为,我们需要假设函数f也是柯里化的:

To obtain the behavior of the built-in fold_left, we need to assume that the function f is also curried:

# 让 rec fold_left fil =

# let rec fold_left f i l =

 匹配 l

 match l with

 | [] -> 我

 | [] -> i

 | h :: t -> fold_left f (fih) t;;

 | h :: t -> fold_left f (f i h) t;;

val fold_left: ('a -> 'b -> 'a) -> 'a -> 'b 列表 -> 'a = <fun>

val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>

# fold_left curried_plus 0 [1;2;3;4;5];;

# fold_left curried_plus 0 [1;2;3;4;5];;

- :int = 15

- : int = 15

再次注意函数推断类型的差异。■

Note again the difference in the inferred type of the functions. ■

当然,也可以通过在函数主体内嵌套显式fun符号来定义fold_left 。但是,使用并列参数的简写符号更加直观和方便。

It is of course possible to define fold_left by nesting occurrences of the explicit fun notation within the function's body. The shorthand notation, with juxtaposed arguments, however, is substantially more intuitive and convenient.

例 11.75

Example 11.75

OCaml 与 Scheme 中的柯里化

Currying in OCaml vs Scheme

还要注意,OCaml 的函数调用语法(函数和参数的并置)使柯里化函数的使用比在 Scheme 中更直观、更方便:

Note also that OCaml's syntax for function calls—juxtaposition of function and argument—makes the use of a curried function more intuitive and convenient than it is in Scheme:

fold_left (+) 0 [1; 2; 3; 4; 5];(* OCaml *)
(((柯里化折叠 +) 0) '(1 2 3 4 5)); 方案

11.7 理论基础

11.7 Theoretical Foundations

例 11.76

Example 11.76

声明性(非构造性)函数定义

Declarative (nonconstructive) function definition

从数学上讲,函数是单值映射:它将一个集合(定义域)中的每个元素与另一个集合(值域)中的(最多)一个元素相关联。在传统符号中,我们用以下方式表示平方根函数的定义域和值域:

Mathematically, a function is a single-valued mapping: it associates every element in one set (the domain) with (at most) one element in another set (the range). In conventional notation, we indicate the domain and range of, say, the square root function by writing

平方根RR

sqrt:RR

si1_e

我们还可以使用传统的集合符号来定义函数:

We can also define functions using conventional set notation:

平方根{R×R|>0=2}

sqrt{(x,y)R×R|y>0x=y2}

si2_e

不幸的是,这种符号是非构造性的:它没有告诉我们如何计算平方根。Church 设计了 ​​lambda 演算来解决这个限制。■

Unfortunately, this notation is nonconstructive: it doesn't tell us how to compute square roots. Church designed the lambda calculus to address this limitation. ■

11-02-9780124104099 更深入地

IN MORE DEPTH

Lambda 演算是一种函数定义的构造符号。我们在配套网站上对此进行了更详细的讨论。任何可计算函数都可以写成lambda 表达式。计算相当于将参数宏替换到函数定义中,然后通过简单而机械的重写规则将其简化为最简形式。应用这些规则的顺序反映了应用顺序和正常顺序求值之间的区别,如第6.6.2 节所述。某些简单函数(例如恒等函数)的使用约定允许将选择、结构甚至算术捕获为 lambda 表达式。递归是通过不动点的概念来捕获的。

Lambda calculus is a constructive notation for function definitions. We consider it in more detail on the companion site. Any computable function can be written as a lambda expression. Computation amounts to macro substitution of arguments into the function definition, followed by reduction to simplest form via simple and mechanical rewrite rules. The order in which these rules are applied captures the distinction between applicative and normal-order evaluation, as described in Section 6.6.2. Conventions on the use of certain simple functions (e.g., the identity function) allow selection, structures, and even arithmetic to be captured as lambda expressions. Recursion is captured through the notion of fixed points.

11.8 函数式编程的视角

11.8 Functional Programming in Perspective

无副作用编程是一个非常有吸引力的想法。如第 6.1.26.3节所述,副作用会使程序难读且难以编译。相比之下,没有副作用则使表达式在引用上透明— 与求值顺序无关。纯函数式语言的程序员和编译器可以使用方程推理,其中两个表达式在任何时间点的等价意味着它们在始终等价。反过来,方程推理对于并行执行非常有吸引力:在纯函数式语言中,函数的参数可以安全地彼此并行求值。在惰性函数式语言中,它们可以与传递它们的函数(的开头)并行求值。我们将在第13.4.5 节中进一步考虑这些可能性。

Side-effect-free programming is a very appealing idea. As discussed in Sections 6.1.2 and 6.3, side effects can make programs both hard to read and hard to compile. By contrast, the lack of side effects makes expressions referentially transparent—independent of evaluation order. Programmers and compilers of a purely functional language can employ equational reasoning, in which the equivalence of two expressions at any point in time implies their equivalence at all times. Equational reasoning in turn is highly appealing for parallel execution: In a purely functional language, the arguments to a function can safely be evaluated in parallel with each other. In a lazy functional language, they can be evaluated in parallel with (the beginning of) the function to which they are passed. We will consider these possibilities further in Section 13.4.5.

不幸的是,在常见的编程习惯用法中,典型的副作用——赋值——起着核心作用。函数式编程的批评者经常指出这些习惯用法是需要命令式语言特性的证据。I/O 就是一个例子。我们已经看到(在第 11.5 节中),可以使用流以函数式方式对文件的顺序访问进行建模。对于图形和随机文件访问,我们还看到 Haskell 的 monad 可以干净地将操作的调用与语言的主体隔离开来,并允许将方程推理的全部功能应用于值的计算和 I/O 操作发生顺序的确定。

Unfortunately, there are common programming idioms in which the canonical side effect—assignment—plays a central role. Critics of functional programming often point to these idioms as evidence of the need for imperative language features. I/O is one example. We have seen (in Section 11.5) that sequential access to files can be modeled in a functional manner using streams. For graphics and random file access we have also seen that the monads of Haskell can cleanly isolate the invocation of actions from the bulk of the language, and allow the full power of equational reasoning to be applied to both the computation of values and the determination of the order in which I/O actions should occur.

其他常见的“自然命令式”习语的例子包括

Other commonly cited examples of “naturally imperative” idioms include

复杂结构的初始化:Lisp 和 ML 系列对列表的严重依赖反映了函数可以轻松地从旧列表的组件构建新列表。其他数据结构(尤其是多维数组)则不那么容易逐步组合,特别是如果初始化元素的自然顺序不是严格的行优先或列优先。

Initialization of complex structures: The heavy reliance on lists in the Lisp and ML families reflects the ease with which functions can build new lists out of the components of old lists. Other data structures—multidimensional arrays in particular—are much less easy to put together incrementally, particularly if the natural order in which to initialize the elements is not strictly row-major or column-major.

摘要:许多程序都包含扫描大型数据结构或大量输入数据的代码,用于计算各种项目或模式的出现次数。跟踪计数的自然方法是使用字典数据结构,在该结构中,人们会反复更新与最近注意到的键相关联的计数。

Summarization: Many programs include code that scans a large data structure or a large amount of input data, counting the occurrences of various items or patterns. The natural way to keep track of the counts is with a dictionary data structure in which one repeatedly updates the count associated with the most recently noticed key.

就地变异:在具有非常大数据集的程序中,必须尽可能节省内存使用量,以最大限度地增加内存或缓存中可容纳的数据量。例如,排序程序需要就地排序,而不是将元素复制到新数组或列表中。同样,基于矩阵的科学程序需要就地更新值。

In-place mutation: In programs with very large data sets, one must economize as much as possible on memory usage, to maximize the amount of data that will fit in memory or the cache. Sorting programs, for example, need to sort in place, rather than copying elements to a new array or list. Matrix-based scientific programs, likewise, need to update values in place.

最后这三个习惯用法是所谓的平凡更新问题的例子。如果使用函数式语言时,底层实现每次必须更改其元素之一时都强制创建整个数据结构的新副本,那么结果将非常低效。在命令式程序中,通过允许就地修改现有结构可以避免此问题。

These last three idioms are examples of what has been called the trivial update problem. If the use of a functional language forces the underlying implementation to create a new copy of the entire data structure every time one of its elements must change, then the result will be very inefficient. In imperative programs, the problem is avoided by allowing an existing structure to be modified in place.

有人可能会说,虽然琐碎的更新问题给 Lisp 及其相关语言带来了麻烦,但它并不反映函数式编程本身的固有弱点。解决方案需要结合方便的符号(用于访问复杂结构的任意元素)和能够确定旧版本结构何时不再使用的实现,以便可以就地更新而不是复制。

One can argue that while the trivial update problem causes trouble in Lisp and its relatives, it does not reflect an inherent weakness of functional programming per se. What is required for a solution is a combination of convenient notation—to access arbitrary elements of a complex structure—and an implementation that is able to determine when the old version of the structure will never be used again, so it can be updated in place instead of being copied.

Sisal、pH 和单赋值 C (SAC) 将数组类型和迭代语法与纯函数语义相结合。迭代构造被定义为尾递归函数的语法糖。嵌套时,这些构造可轻松用于初始化多维数组。该语言的语义表明循环的每次迭代都会返回整个数组的新副本。但是,编译器可以轻松验证返回后从未​​使用过旧副本,因此可以安排就地执行所有更新。在没有命令式语法的情况下也可以执行类似的优化,但需要更复杂的分析。Cann 报告称,Livermore Sisal 编译器在标准数值基准测试中,能够消除 99% 到 100% 的所有复制操作 [ Can92 ]。Scholz 报告称,SAC 的性能可与经过精心优化的现代 Fortran 程序相媲美 [ Sch03 ]。

Sisal, pH, and Single Assignment C (SAC) combine array types and iterative syntax with purely functional semantics. The iterative constructs are defined as syntactic sugar for tail-recursive functions. When nested, these constructs can easily be used to initialize a multidimensional array. The semantics of the language say that each iteration of the loop returns a new copy of the entire array. The compiler can easily verify, however, that the old copy is never used after the return, and can therefore arrange to perform all updates in place. Similar optimizations could be performed in the absence of the imperative syntax, but require somewhat more complex analysis. Cann reports that the Livermore Sisal compiler was able to eliminate 99 to 100 percent of all copy operations in standard numeric benchmarks [Can92]. Scholz reports performance for SAC competitive with that of carefully optimized modern Fortran programs [Sch03].

设计与实现

Design & Implementation

11.7 副作用和编译

11.7 Side effects and compilation

如第 11.2 节所述,副作用自由具有很强的概念吸引力:它使程序员无需担心未记录的非局部变量访问、乱序更新、别名和悬空指针。至少在理论上,副作用自由还有可能允许编译器生成更快的代码:与别名一样,副作用通常会阻止在寄存器中缓存值(第 3.5.1 节)或使用常量和副本传播(第 C-17.3 和 C-17.4 节)。

As noted in Section 11.2, side-effect freedom has a strong conceptual appeal: it frees the programmer from concern over undocumented access to nonlocal variables, misordered updates, aliases, and dangling pointers. Side-effect freedom also has the potential, at least in theory, to allow the compiler to generate faster code: like aliases, side effects often preclude the caching of values in registers (Section 3.5.1) or the use of constant and copy propagation (Sections C-17.3 and C-17.4).

那么,生成函数式程序的快速代码的技术障碍是什么呢?琐碎的更新问题当然是一个挑战,无限范围的值的堆管理成本也是如此。类型检查在 Lisp 衍生的语言中会产生很大的运行时成本,但在 ML 衍生的语言中则不会。在 Miranda 和 Haskell 中,记忆化的成本很高,尽管所谓的严格性分析可能允许编译器在应用顺序评估可证明等价的情况下消除它。这些挑战都是持续研究的主题。

So what are the technical obstacles to generating fast code for functional programs? The trivial update problem is certainly a challenge, as is the cost of heap management for values with unlimited extent. Type checking imposes significant run-time costs in languages descended from Lisp, but not in those descended from ML. Memoization is expensive in Miranda and Haskell, though so-called strictness analysis may allow the compiler to eliminate it in cases where applicative order evaluation is provably equivalent. These challenges are all the subject of continuing research.

近年来,函数式编程的理论和实践都取得了长足的进步。Wadler [ Wad98b ] 在 20 世纪 90 年代末指出,广泛采用函数式语言的主要障碍是社会和商业方面的,而不是技术方面的:大多数程序员都接受过命令式风格的训练;函数式编程的软件库和开发环境尚不如命令式语言成熟。过去十年的经验似乎证实了这一特点:随着更好工具的开发和实践经验的不断积累,函数式语言开始得到更广泛的应用。函数式特性也开始出现在 C#、Python 和 Ruby 等主流命令式语言中。

Significant strides in both the theory and practice of functional programming have been made in recent years. Wadler [Wad98b] argued in the late 1990s that the principal remaining obstacles to the widespread adoption of functional languages were social and commercial, not technical: most programmers have been trained in an imperative style; software libraries and development environments for functional programming are not yet as mature as those of their imperative cousins. Experience over the past decade appears to have borne out this characterization: with the development of better tools and a growing body of practical experience, functional languages have begun to see much wider use. Functional features have also begun to appear in such mainstream imperative languages as C#, Python, and Ruby.

11-01-9780124104099检查你的理解

Check Your Understanding

19. 正常顺序应用顺序求值有什么区别?什么是惰性求值?

19. What is the difference between normal-order and applicative-order evaluation? What is lazy evaluation?

20. Scheme 中的函数和 特殊形式有什么区别?

20. What is the difference between a function and a special form in Scheme?

21.函数 严格是什么意思?

21. What does it mean for a function to be strict?

22. 什么是记忆化

22. What is memoization?

23. 如何在纯函数式编程模型中容纳 I/O?

23. How can one accommodate I/O in a purely functional programming model?

24. 什么是高阶函数(也称为函数形式)?请举三个例子。

24. What is a higher-order function (also known as a functional form)?. Give three examples.

25. 什么是柯里化?它在实际程序中起什么作用?

25. What is currying? What purpose does it serve in practical programs?

26. 函数式编程中的简单更新问题是什么?

26. What is the trivial update problem in functional programming?

27. 总结支持和反对无副作用编程的论点。

27. Summarize the arguments for and against side-effect-free programming.

28. 为什么函数式语言大量使用列表?

28. Why do functional languages make such heavy use of lists?

11.9 总结和结束语

11.9 Summary and Concluding Remarks

在本章中,我们重点讨论了函数式计算模型。命令式程序主要通过迭代和副作用(即修改变量)进行计算,而函数式程序主要通过将参数代入函数进行计算。我们首先列举了函数式编程中的一系列关键问题,包括一等函数和高阶函数、多态性、控制流和求值顺序以及支持基于列表的数据。然后,我们转向了两个具体的例子——Lisp 的 Scheme 方言和 ML 的 OCaml 方言——以了解如何在编程语言中解决这些问题。我们还简要介绍了 Haskell 中的惰性求值和 monad。

In this chapter we have focused on the functional model of computing. Where an imperative program computes principally through iteration and side effects (i.e., the modification of variables), a functional program computes principally through substitution of parameters into functions. We began by enumerating a list of key issues in functional programming, including first-class and higher-order functions, polymorphism, control flow and evaluation order, and support for list-based data. We then turned to a pair of concrete examples—the Scheme dialect of Lisp and the OCaml dialect of ML—to see how these issues may be addressed in a programming language. We also considered, more briefly, the lazy evaluation and monads found in Haskell.

对于命令式编程语言,底层形式模型通常被认为是图灵机。对于函数式语言,该模型是 lambda 演算。这两种模型都是在数学界发展起来的,作为形式化有效程序概念的一种手段,用于构造性证明。除了硬件对算术精度、磁盘和内存空间等的限制外,lambda 演算的全部功能在函数式语言中都可用。虽然对 lambda 演算的完整处理很容易占用另一本书,但我们在配套网站上提供了概述。我们考虑了重写规则、求值顺序和 Church-Rosser 定理。我们注意到,使用非常简单的符号的约定提供了整数算术、选择、递归和结构化数据类型的计算能力。

For imperative programming languages, the underlying formal model is often taken to be a Turing machine. For functional languages, the model is the lambda calculus. Both models evolved in the mathematical community as a means of formalizing the notion of an effective procedure, as used in constructive proofs. Aside from hardware-imposed limits on arithmetic precision, disk and memory space, and so on, the full power of lambda calculus is available in functional languages. While a full treatment of the lambda calculus could easily consume another book, we provided an overview on the companion site. We considered rewrite rules, evaluation order, and the Church-Rosser theorem. We noted that conventions on the use of very simple notation provide the computational power of integer arithmetic, selection, recursion, and structured data types.

出于实际原因,许多函数式语言都扩展了 lambda 演算,增加了其他功能,包括赋值、I/O 和迭代。此外,Lisp 方言是同音的:程序看起来像普通的数据结构,可以即时创建、修改和执行。

For practical reasons, many functional languages extend the lambda calculus with additional features, including assignment, I/O, and iteration. Lisp dialects, moreover, are homoiconic: programs look like ordinary data structures, and can be created, modified, and executed on the fly.

列表在大多数函数式程序中占有重要地位,主要是因为它们可以轻松地逐步构建,而无需将分配和修改状态作为单独的操作。许多函数式语言也提供其他结构化数据类型。在 Sisal 和单赋值 C 中,强调迭代语法、尾递归语义和高性能编译器,使基于多维数组的函数式程序能够实现与命令式程序相当的性能。

Lists feature prominently in most functional programs, largely because they can easily be built incrementally, without the need to allocate and then modify state as separate operations. Many functional languages provide other structured data types as well. In Sisal and Single Assignment C, an emphasis on iterative syntax, tail-recursive semantics, and high-performance compilers allows multidimensional array-based functional programs to achieve performance comparable to that of imperative programs.

11.10 练习

11.10 Exercises

11.1 Scheme 的 define原语是命令式语言特性吗?为什么是或为什么不是?

11.1 Is the define primitive of Scheme an imperative language feature? Why or why not?

11.2 可以用命令式语言(如 C)的纯函数式子集编写程序,但该语言的某些局限性很快就会显现出来。需要向你最喜欢的命令式语言添加哪些功能才能使其真正成为有用的函数式语言?(提示:Scheme 有什么是 C 所缺乏的?)

11.2 It is possible to write programs in a purely functional subset of an imperative language such as C, but certain limitations of the language quickly become apparent. What features would need to be added to your favorite imperative language to make it genuinely useful as a functional language? (Hint: What does Scheme have that C lacks?)

11.3 解释短路布尔表达式和正常顺序求值之间的联系。为什么cond在 Scheme 中是一种特殊形式,而不是函数?

11.3 Explain the connection between short-circuit Boolean expressions and normal-order evaluation. Why is cond a special form in Scheme, rather than a function?

11.4用你最喜欢的命令式语言编写一个程序,该程序具有与 图 11.1中的 Scheme 程序相同的输入和输出。根据你的经验,你能对 Scheme 在符号计算方面的实用性做出一般性观察吗?

11.4 Write a program in your favorite imperative language that has the same input and output as the Scheme program of Figure 11.1. Can you make any general observations about the usefulness of Scheme for symbolic computation, based on your experience?

11.5 假设我们希望从列表中删除相邻的重复元素(例如,在排序之后)。以下 Scheme 函数可实现此目标:(define unique (lambda (L) (cond ((null? L) L) ((null? (cdr L)) L) ((eqv? (car L) (car (cdr L))) (unique (cdr L))) (else (cons (car L) (unique (cdr L)))))))编写一个类似的函数,使用 Scheme 的命令式特性“就地”修改L,而不是构建新列表。将您的函数与上面的代码在简洁性、概念清晰度和速度方面进行比较。

 

  

   

    

    

    

    

11.5 Suppose we wish to remove adjacent duplicate elements from a list (e.g., after sorting). The following Scheme function accomplishes this goal:

 (define unique

  (lambda (L)

   (cond

    ((null? L) L)

    ((null? (cdr L)) L)

    ((eqv? (car L) (car (cdr L))) (unique (cdr L)))

    (else (cons (car L) (unique (cdr L)))))))

Write a similar function that uses the imperative features of Scheme to modify L “in place,” rather than building a new list. Compare your function to the code above in terms of brevity, conceptual clarity, and speed.

11.6 写出下面的尾递归版本:

11.6 Write tail-recursive versions of the following:

(a) ;; 计算以 2 为底的整数对数;;(二进制表示的位数);; 仅适用于正整数(定义 log2 (lambda(n)(if(= n 1)1(+ 1(log2(商 n 2)))))) 

 

 

 

  

   

(a)  ;; compute integer log, base 2

 ;; (number of bits in binary representation)

 ;; works only for positive integers

 (define log2

  (lambda (n)

   (if (= n 1) 1 (+ 1 (log2 (quotient n 2))))))

(b)  ;; 在列表中查找最小元素(define min (lambda (l) (cond ((null? l) '()) ((null? (cdr l)) (car l)) (#t (let ((a (car l)) (b (min (cdr l)))) (if (< ba) ba)))))) 

 

  

   

    

    

    

      

     

(b)  ;; find minimum element in a list

 (define min

  (lambda (l)

   (cond

    ((null? l) '())

    ((null? (cdr l)) (car l))

    (#t (let ((a (car l))

      (b (min (cdr l))))

     (if (< b a) b a))))))

11.7 编写纯函数式 Scheme 函数

11.7 Write purely functional Scheme functions to

(a) 返回给定列表的所有旋转。例如,(rotate '(abcde))应返回((abcde) (bcdea) (cdeab) (deabc) (eabcd))(按某种顺序)。

(a) return all rotations of a given list. For example, (rotate '(a b c d e)) should return ((a b c d e) (b c d e a) (c d e a b) (d e a b c) (e a b c d)) (in some order).

(b) 返回一个列表,其中包含给定列表中满足给定谓词的所有元素。例如,(filter (lambda (x) (< x 5)) '(3 9 5 8 2 4 7))应返回(3 2 4)

(b) return a list containing all elements of a given list that satisfy a given predicate. For example, (filter (lambda (x) (< x 5)) '(3 9 5 8 2 4 7)) should return (3 2 4).

11.8 编写一个纯函数式 Scheme 函数,返回给定列表的所有排列列表。例如,给定(abc),它应该返回((abc) (bac) (bca) (acb) (cab) (cba)) (按某种顺序)。

11.8 Write a purely functional Scheme function that returns a list of all permutations of a given list. For example, given (a b c), it should return ((a b c) (b a c) (b c a) (a c b) (c a b) (c b a)) (in some order).

11.9修改 图 11.1中的 Scheme 程序或图 11.3中的 OCaml 程序,使其模拟 NFA(非确定性有限自动机),而不是 DFA。(这些自动机之间的区别在2.2.1 节中描述。​​)由于面对多值时你无法正确“猜测”,转换函数,您将需要使用明确编码的回溯来搜索接受的一系列移动(如果有的话),或者跟踪机器在给定时间点可能处于的所有可能状态。

11.9 Modify the Scheme program of Figure 11.1 or the OCaml program of Figure 11.3 to simulate an NFA (nondeterministic finite automaton), rather than a DFA. (The distinction between these automata is described in Section 2.2.1.) Since you cannot “guess” correctly in the face of a multivalued transition function, you will need either to use explicitly coded backtracking to search for an accepting series of moves (if there is one), or keep track of all possible states that the machine could be in at a given point in time.

11.10 考虑判断两棵树是否具有相同边缘的问题:无论内部结构如何,具有相同顺序的相同叶子集。解决这个问题的一个显而易见的方法是编写一个函数flatten,它以一棵树作为参数并返回其叶子的有序列表。然后我们可以说(define same-fringe (lambda (T1 T2) (equal (flatten T1) (flatten T2))))在 Scheme 中编写一个简单版本的flatten 。当两棵树的前几个叶子不同时, same-fringe 的效率如何?在像 Haskell 这样对所有参数都使用惰性求值的语言中,你的答案会有什么不同?使用延迟强制,在 Scheme 中获得 Haskell 的行为有多难?

 

  

   

11.10 Consider the problem of determining whether two trees have the same fringe: the same set of leaves in the same order, regardless of internal structure. An obvious way to solve this problem is to write a function flatten that takes a tree as argument and returns an ordered list of its leaves. Then we can say

 (define same-fringe

  (lambda (T1 T2)

   (equal (flatten T1) (flatten T2))))

Write a straightforward version of flatten in Scheme. How efficient is same-fringe when the trees differ in their first few leaves? How would your answer differ in a language like Haskell, which uses lazy evaluation for all arguments? How hard is it to get Haskell's behavior in Scheme, using delay and force?

11.11 示例 11.59中,我们展示了如何根据流的惰性求值实现交互式 I/O。遗憾的是,我们的代码无法按编写的方式工作,因为 Scheme 使用应用顺序求值。不过,我们可以通过调用delayforce使其工作。

假设我们将input定义为一个返回“istream”的函数——一个承诺,当强制执行时将产生一个对,其cdr是一个 istream: (define input (lambda () (delay (cons (read) (input)))))现在我们可以定义驱动程序以期望一个“ostream”——一个空列表或一个对,cdr是一个 ostream: (define driver (lambda (s) (if (null? s) '() (display (car s)) (driver (force (cdr s))))))注意force的使用说明如何编写函数squares以使其接受 istream 作为参数并返回 ostream。然后您应该能够输入(driver(squares(input)))并看到适当的行为。

 



 

  

   

    

    



11.11 In Example 11.59 we showed how to implement interactive I/O in terms of the lazy evaluation of streams. Unfortunately, our code would not work as written, because Scheme uses applicative-order evaluation. We can make it work, however, with calls to delay and force.

Suppose we define input to be a function that returns an “istream”—a promise that when forced will yield a pair, the cdr of which is an istream:

 (define input (lambda () (delay (cons (read) (input)))))

Now we can define the driver to expect an “ostream”—an empty list or a pair, the cdr of which is an ostream:

 (define driver

  (lambda (s)

   (if (null? s) '()

    (display (car s))

    (driver (force (cdr s))))))

Note the use of force.

Show how to write the function squares so that it takes an istream as argument and returns an ostream. You should then be able to type (driver (squares (input))) and see appropriate behavior.

11.12 编写操作流的conscarcdr的新版本。使用它们重写上一个练习的代码,以消除对delayforce 的调用。请注意,流版本的cons将需要避免评估其第二个参数;您需要学习如何在 Scheme 中定义宏(派生的特殊形式)。

11.12 Write new versions of cons, car, and cdr that operate on streams. Using them, rewrite the code of the previous exercise to eliminate the calls to delay and force. Note that the stream version of cons will need to avoid evaluating its second argument; you will need to learn how to define macros (derived special forms) in Scheme.

11.13 用 Scheme 编写标准快速排序算法,不要使用任何命令式语言特性。小心避免琐碎的更新问题;您的代码应在预期时间内运行n log n

使用数组重写您的代码(您可能需要查阅 Scheme 手册以获取更多信息)。比较两种排序的运行时间和空间要求。

11.13 Write the standard quicksort algorithm in Scheme, without using any imperative language features. Be careful to avoid the trivial update problem; your code should run in expected time n log n.

Rewrite your code using arrays (you will probably need to consult a Scheme manual for further information). Compare the running time and space requirements of your two sorts.

11.14 编写插入查找例程来操作 Scheme 中的二叉搜索树(如果需要更多信息,请查阅算法文本)。解释为什么简单的更新问题不会影响插入的渐近性能。

11.14 Write insert and find routines that manipulate binary search trees in Scheme (consult an algorithms text if you need more information). Explain why the trivial update problem does not impact the asymptotic performance of insert.

11.15 用纯函数式 Scheme 编写一个 LL(1) 解析器生成器。如果你参考图 2.24,请记住你需要使用尾递归代替迭代。假设输入 CFG 由一个列表列表组成,每个列表对应一个语法中的非终结符。每个子列表的第一个元素应该是非终结符;其余元素应该是产生式的右侧,其中非终结符是左侧。你可以假设起始符号的子列表将是列表中的第一个。如果我们使用带引号的字符串来表示语法符号,则图 2.16中的计算器语法将如下所示:'((“program” (“stmt_list” “$$”)) (“stmt_list” (“stmt” “stmt_list”) ()) (“stmt” (“id” “:=“ “expr”) (“read” “id”) (“write” “expr”)) (“expr” (“term” “term_tail”)) (“term” (“factor” “factor_tail”)) (“term_tail” (“add_op” “term” “term_tail”) ()) (“factor_tail” (“mult_op” “factor” “FT”) ()) (“add_op” (“+”) (“−”)) (“mult_op” (“*”) (“/”)) (“factor” (“id”) (“number”) (“(“ “expr” “)”)))您的输出应为解析表具有相同的格式,不同之处在于每个右侧都被一(2 元素列表)替换,其第一个元素是相应生产的预测集,其第二个元素是右侧。对于计算器语法,表格如下所示:((“program” ((“$$” “id” “read” “write”) (“stmt_list” “$$”))) (“stmt_list” ((“id” “read” “write”) (“stmt” “stmt_list”)) ((“$$”) ())) (“stmt” ((“id”) (“id” “:=“ “expr”)) ((“read”) (“read” “id”)) ((“write”) (“write” “expr”))) (“expr” ((“(“ “id” “number”) (“term” “term_tail”))) (“term” ((“(“ “id” “number”) (“factor” “factor_tail”))) (“term_tail” ((“+” “−”) (“add_op” “term” “term_tail”)) ((“$$” “)” “id” “read” “write”) ())) (“factor_tail” ((“*” “/”) (“mult_op” “factor” “factor_tail”)) ((“$$” “)” “+” “−” “id” “read” “write”) ())) (“add_op” ((“+”) (“+”)) ((“−”) (“−”))) (“mult_op” ((“*”) (“*”)) ((“/”) (“/”))) (“factor” ((“id”) (“id” )) ((“number”) (“number”)) ((“(“) (“ “expr” “)”))))

 

  

  

  

  

  

  

  

  

  



 

  

   

   

  

   

   

   

  

 

 

  

  

 

  

  

 

 

 

  

  

  

(提示:您可能想要定义一个right_context函数,该函数以非终结符B作为参数并返回所有对 ( A , β )的列表,其中A是非终结符,β是符号列表,这样对于某些可能不同的符号列表αAα B β。此函数对于计算 FOLLOW 集很有用。您可能还想构建一个尾递归函数,重新计算 FIRST 和 FOLLOW 集直到它们收敛。如果您不将ε包含在任何一个集合中,而是为每个非终结符保留一个单独的估计值,以确定它是否可能生成ε ,您会发现这样做更容易。)

11.15 Write an LL(1) parser generator in purely functional Scheme. If you consult Figure 2.24, remember that you will need to use tail recursion in place of iteration. Assume that the input CFG consists of a list of lists, one per nonterminal in the grammar. The first element of each sublist should be the nonterminal; the remaining elements should be the right-hand sides of the productions for which that nonterminal is the left-hand side. You may assume that the sublist for the start symbol will be the first one in the list. If we use quoted strings to represent grammar symbols, the calculator grammar of Figure 2.16 would look like this:

 '((“program” (“stmt_list” “$$”))

  (“stmt_list” (“stmt” “stmt_list”) ())

  (“stmt” (“id” “:=“ “expr”) (“read” “id”) (“write” “expr”))

  (“expr” (“term” “term_tail”))

  (“term” (“factor” “factor_tail”))

  (“term_tail” (“add_op” “term” “term_tail”) ())

  (“factor_tail” (“mult_op” “factor” “FT”) ())

  (“add_op” (“+”) (“−”))

  (“mult_op” (“*”) (“/”))

  (“factor” (“id”) (“number”) (“(“ “expr” “)”)))

Your output should be a parse table that has this same format, except that every right-hand side is replaced by a pair (a 2-element list) whose first element is the predict set for the corresponding production, and whose second element is the right-hand side. For the calculator grammar, the table looks like this:

 ((“program” ((“$$” “id” “read” “write”) (“stmt_list” “$$”)))

  (“stmt_list”

   ((“id” “read” “write”) (“stmt” “stmt_list”))

   ((“$$”) ()))

  (“stmt”

   ((“id”) (“id” “:=“ “expr”))

   ((“read”) (“read” “id”))

   ((“write”) (“write” “expr”)))

  (“expr” ((“(“ “id” “number”) (“term” “term_tail”)))

 (“term” ((“(“ “id” “number”) (“factor” “factor_tail”)))

 (“term_tail”

  ((“+” “−”) (“add_op” “term” “term_tail”))

  ((“$$” “)” “id” “read” “write”) ()))

 (“factor_tail”

  ((“*” “/”) (“mult_op” “factor” “factor_tail”))

  ((“$$” “)” “+” “−” “id” “read” “write”) ()))

 (“add_op” ((“+”) (“+”)) ((“−”) (“−”)))

 (“mult_op” ((“*”) (“*”)) ((“/”) (“/”)))

 (“factor”

  ((“id”) (“id”))

  ((“number”) (“number”))

  ((“(“) (“(“ “expr” “)”))))

(Hint: You may want to define a right_context function that takes a nonterminal B as argument and returns a list of all pairs (A, β), where A is a nonterminal and β is a list of symbols, such that for some potentially different list of symbols α, Aα B β. This function is useful for computing FOLLOW sets. You may also want to build a tail-recursive function that recomputes FIRST and FOLLOW sets until they converge. You will find it easier if you do not include ε in either set, but rather keep a separate estimate, for each nonterminal, of whether it may generate ε.)

11.16 编写一个相等运算符(称为=/),使它能够正确地对示例 11.38中的yearday类型起作用。(您可能需要查找控制闰年发生的规则。)

11.16 Write an equality operator (call it =/) that works correctly on the yearday type of Example 11.38. (You may need to look up the rules that govern the occurrence of leap years.)

11.17 为侧栏 11.3 中介绍的摄氏和华氏温度类型创建加法和减法函数。为了允许混合使用这两种刻度,您还应该定义转换函数ct_of_ft : fahrenheit_temp -> celsius_tempft_of_ct : celsius_temp -> fahrenheit_temp。您的转换应该四舍五入到最接近的度数(半度向上舍入)。

11.17 Create addition and subtraction functions for the celsius and fahrenheit temperature types introduced in Sidebar 11.3. To allow the two scales to be mixed, you should also define conversion functions ct_of_ft : fahrenheit_temp -> celsius_temp and ft_of_ct : celsius_temp -> fahrenheit_temp. Your conversions should round to the nearest degree (half degrees round up).

11.18 我们可以在函数中使用封装来延迟 OCaml 中的求值:

type 'a delayed_list = Pair of 'a * 'a delayed_list | Promise of (unit -> 'a * 'a delayed_list);; let head = function | Pair (h, r) -> h | Promise (f) -> let (a, b) = f() in a;; let rest = function | Pair (h, r) -> r | Promise (f) -> let (a, b) = f() in b;;现在给定let rec next_int n = (n, Promise (fun() -> next_int (n + 1)));; let naturals = Promise (fun() -> next_int (1));;我们有

  

 

 

 

 

 

 

 



 

 

11.18 We can use encapsulation within functions to delay evaluation in OCaml:

type 'a delayed_list =

  Pair of 'a * 'a delayed_list

 | Promise of (unit -> 'a * 'a delayed_list);;

 let head = function

 | Pair (h, r) -> h

 | Promise (f) -> let (a, b) = f() in a;;

 let rest = function

 | Pair (h, r) -> r

 | Promise (f) -> let (a, b) = f() in b;;

Now given

 let rec next_int n = (n, Promise (fun() -> next_int (n + 1)));;

 let naturals = Promise (fun() -> next_int (1));;

we have

头部自然;;⇒ 1
头部(自然休息);;⇒ 2
头部(休息(自然休息));;⇒ 3
 

延迟列表naturals的长度实际上是无限的。它只会计算出实际需要的值。但是,如果一个值需要多次,则每次都会重新计算。说明如何使用指针和赋值(示例 8.42 )来记忆delayed_list的值,以便元素只计算一次。

The delayed list naturals is effectively of unlimited length. It will be computed out only as far as actually needed. If a value is needed more than once, however, it will be recomputed every time. Show how to use pointers and assignment (Example 8.42) to memoize the values of a delayed_list, so that elements are computed only once.

11.19 编写示例11.67的OCaml版本。或者(或另外),用OCaml解决练习11.5、11.7、11.8、11.10、11.13、11.1411.15

11.19 Write an OCaml version of Example 11.67. Alternatively (or in addition), solve Exercises 11.5, 11.7, 11.8, 11.10, 11.13, 11.14, or 11.15 in OCaml.

11-02-9780124104099 11.20–11.23 更深入。

11.20–11.23  In More Depth.

11.11 探索

11.11 Explorations

11.24 阅读 Lisp 的原始自我定义 [ MAE + 65 ]。将其与 Scheme 的类似定义 [ AS96第 4 章] 进行比较。有什么不同?什么保持不变?在每个定义中, applyeval中内置了什么?你觉得整个想法怎么样?元循环解释器真的定义了什么吗?还是说它是“循环推理”?

11.24 Read the original self-definition of Lisp [MAE+65]. Compare it to a similar definition of Scheme [AS96, Chap. 4]. What is different? What has stayed the same? What is built into apply and eval in each definition? What do you think of the whole idea? Does a metacircular interpreter really define anything, or is it “circular reasoning”?

11.25 阅读 John Backus 的图灵奖演讲 [ Bac78 ],他在演讲中主张函数式编程。他的 FP 符号与 Lisp 和 ML 语言家族相比如何?

11.25 Read the Turing Award lecture of John Backus [Bac78], in which he argues for functional programming. How does his FP notation compare to the Lisp and ML language families?

11.26 进一步了解 Haskell 中的 monad。特别注意列表的定义。解释列表 monad 与列表推导(示例 8.58)、迭代器、延续(第 6.2.2 节)和回溯搜索的关系。

11.26 Learn more about monads in Haskell. Pay particular attention to the definition of lists. Explain the relationship of the list monad to list comprehensions (Example 8.58), iterators, continuations (Section 6.2.2), and backtracking search.

11.27 提前阅读并了解事务内存第 13.4.4 节)。然后阅读 STM Haskell [ HMPH05 ]。解释 monad 如何促进对线程间共享位置的更新序列化。

11.27 Read ahead and learn about transactional memory (Section 13.4.4). Then read up on STM Haskell [HMPH05]. Explain how monads facilitate the serialization of updates to locations shared between threads.

11.28 我们已经看到 Lisp 和 ML 包含赋值和迭代等命令式特性。这些特性有多重要?Haskell 等语言坚持纯函数式编程风格,会放弃什么(反过来说,它们会得到什么)?同样,您如何看待最近几种命令式语言(尤其是 Python 和 C#——参见边栏 11.6)尝试使用函数构造函数和无限扩展来促进函数式编程?

11.28 We have seen that Lisp and ML include such imperative features as assignment and iteration. How important are these? What do languages like Haskell give up (conversely, what do they gain) by insisting on a purely functional programming style? In a similar vein, what do you think of attempts in several recent imperative languages (notably Python and C#—see Sidebar 11.6) to facilitate functional programming with function constructors and unlimited extent?

11.29 研究函数式程序的编译。会出现哪些特殊问题?用什么技术来解决这些问题?你可以​​从 Appel [ App97 ]、Wilhelm 和 Maurer [ WM95 ] 以及 Grune 等人的编译器文本 [ GBJ + 12 ] 开始搜索。

11.29 Investigate the compilation of functional programs. What special issues arise? What techniques are used to address them? Starting places for your search might include the compiler texts of Appel [App97], Wilhelm and Maurer [WM95], and Grune et al. [GBJ+12].

11-02-9780124104099 11.30–11.32 更深入地了解。

11.30–11.32  In More Depth.

11.12 书目注释

11.12 Bibliographic Notes

Lisp 是最初的函数式编程语言,起源于 McCarthy 及其同事在 20 世纪 50 年代末的工作。Erlang、Haskell、Lisp、Miranda、ML、OCaml、Scheme、Single Assignment C 和 Sisal 的参考书目可在附录 A中找到。历史上重要的 Lisp 方言包括 Lisp 1.5 [ MAE + 65 ]、MacLisp [ Moo78 ](与 Apple Macintosh 无关)和 Interlisp [ TM81 ]。

Lisp, the original functional programming language, dates from the work of McCarthy and his associates in the late 1950s. Bibliographic references for Erlang, Haskell, Lisp, Miranda, ML, OCaml, Scheme, Single Assignment C, and Sisal can be found in Appendix A. Historically important dialects of Lisp include Lisp 1.5 [MAE+65], MacLisp [Moo78] (no relation to the Apple Macintosh), and Interlisp [TM81].

Abelson 和 Sussman 编写的书籍 [ AS96 ] 长期以来一直用于 MIT 和其他地方的入门编程课程,它是基本编程概念(尤其是函数式编程)的经典指南。在 Hudak 的论文 [ Hud89 ] 中可以找到更多历史参考,该论文从 Haskell 的角度概述了该领域。

The book by Abelson and Sussman [AS96], long used for introductory programming classes at MIT and elsewhere, is a classic guide to fundamental programming concepts, and to functional programming in particular. Additional historical references can be found in the paper by Hudak [Hud89], which surveys the field from the point of view of Haskell.

1941 年,Church 引入了 lambda 演算 [ Chu41 ]。Curry 和 Feys 的著作 [ CF58 ] 是经典参考书。Barendregt 的书 [ Bar84 ] 是标准的现代参考书。Michaelson [ Mic89 ] 对该形式主义进行了通俗易懂的介绍,并清晰解释了其与 Lisp 和 ML 的关系。Stansifer [ Sta95第 7.6 节] 对定点组合子Y进行了很好的非正式讨论和正确性证明(参见练习 C-11.21)。

The lambda calculus was introduced by Church in 1941 [Chu41]. A classic reference is the text of Curry and Feys [CF58]. Barendregt's book [Bar84] is a standard modern reference. Michaelson [Mic89] provides an accessible introduction to the formalism, together with a clear explanation of its relationship to Lisp and ML. Stansifer [Sta95, Sec. 7.6] provides a good informal discussion and correctness proof for the fixed-point combinator Y (see Exercise C-11.21).

Fortran 的原始开发者之一 John Backus 在 1977 年的图灵奖演讲 [ Bac78 ]中强烈主张转向函数式编程。他的函数式编程符号称为 FP。Peyton Jones [ Pey87Pey92 ]、Wilhelm 和 Maurer [ WM95第 3 章]、Appel [ App97第 15 章] 和 Grune 等人 [ GBJ + 12第 7 章] 讨论了函数式语言的实现。Peyton Jones 关于“awkward squad”的论文 [ Pey01 ] 被广泛认为是 Haskell 中对 monad 的权威介绍。

John Backus, one of the original developers of Fortran, argued forcefully for a move to functional programming in his 1977 Turing Award lecture [Bac78]. His functional programming notation is known as FP. Peyton Jones [Pey87, Pey92], Wilhelm and Maurer [WM95, Chap. 3], Appel [App97, Chap. 15], and Grune et al. [GBJ+12, Chap. 7] discuss the implementation of functional languages. Peyton Jones's paper on the “awkward squad” [Pey01] is widely considered the definitive introduction to monads in Haskell.

尽管 Lisp 诞生于 20 世纪 60 年代初,但直到最近几年,函数式语言才开始在大型商业系统中得到广泛使用。Wadler [ Wad98aWad98b ] 描述了 20 世纪 90 年代末形势开始转变时的情况。许多后续项目的描述都可以在自 2004 年以来每年举办的“函数式编程商业用户”研讨会 (cufp.galois.com) 的论文集中找到。《函数式编程杂志》也出版了一类关于商业用途的特别文章。Armstrong 报告 [ Arm07 ],爱立信 AXD301 是一个由两百多万行 Erlang 代码组成的电话交换系统,它实现了令人惊叹的“九个九”级可靠性 — — 相当于每年停机时间少于 32 毫秒。

While Lisp dates from the early 1960s, it is only in recent years that functional languages have seen widespread use in large commercial systems. Wadler [Wad98a, Wad98b] describes the situation as of the late 1990s, when the tide began to turn. Descriptions of many subsequent projects can be found in the proceedings of the Commercial Users of Functional Programming workshop (cufp.galois.com), held annually since 2004. The Journal of Functional Programming also publishes a special category of articles on commercial use. Armstrong reports [Arm07] that the Ericsson AXD301, a telephone switching system comprising more than two million lines of Erlang code, has achieved an astonishing “nine nines” level of reliability—the equivalent of less than 32 ms of downtime per year.


1阿兰·图灵(1912-1954 年),图灵奖以他的名字命名,他是一位英国数学家、哲学家和计算机预言家。作为二战期间英国密码分析小组的智力领袖,他在破解德国“恩尼格玛”密码和扭转战争局势方面发挥了重要作用。他还帮助奠定了现代计算机科学的理论基础,构思了通用电子计算机,并开创了人工智能领域。战后,他因同性恋而受到迫害,被剥夺了安全许可,被判处“药物治疗”,最后自杀身亡。

1 Alan Turing (1912–1954), after whom the Turing Award is named, was a British mathematician, philosopher, and computer visionary. As intellectual leader of Britain's cryptanalytic group during World War II, he was instrumental in cracking the German “Enigma” code and turning the tide of the war. He also helped lay the theoretical foundations of modern computer science, conceived the general-purpose electronic computer, and pioneered the field of Artificial Intelligence. Persecuted as a homosexual after the war, stripped of his security clearance, and sentenced to “treatment” with drugs, he committed suicide.

2阿隆佐·丘奇(1903-1995)于 1929 年至 1967 年在普林斯顿大学任教,1967 年至 1990 年在加州大学洛杉矶分校任教。在普林斯顿大学期间,他指导了艾伦·图灵、斯蒂芬·克莱恩、迈克尔·拉宾和达纳·斯科特等人的博士论文。他与图灵共同发现了不可判定问题,这是理解数学极限的重大突破。

2 Alonzo Church (1903–1995) was a member of the mathematics faculty at Princeton University from 1929 to 1967, and at UCLA from 1967 to 1990. While at Princeton he supervised the doctoral theses of, among many others, Alan Turing, Stephen Kleene, Michael Rabin, and Dana Scott. His codiscovery, with Turing, of undecidable problems was a major breakthrough in understanding the limits of mathematics.

3给熟悉 Common Lisp 的读者的提醒:Scheme 中的lambda表达式求值为函数。CommonLisp 中的lambda表达式一个函数(或者更准确地说,自动强制为函数,无需求值)。每当lambda表达式作为参数传递或从函数返回时,区别就变得很重要:它们必须在 Common Lisp 中被引用(使用function# ')以防止求值。Common Lisp 还区分符号的和其作为函数的含义;Scheme 不区分:如果符号表示函数,则函数是符号的值。

3 A word of caution for readers familiar with Common Lisp: A lambda expression in Scheme evaluates to a function. A lambda expression in Common Lisp is a function (or, more accurately, is automatically coerced to be a function, without evaluation). The distinction becomes important whenever lambda expressions are passed as parameters or returned from functions: they must be quoted in Common Lisp (with function or #') to prevent evaluation. Common Lisp also distinguishes between a symbol's value and its meaning as a function; Scheme does not: if a symbol represents a function, then the function is the symbol's value.

4 Scheme 和 Common Lisp 之间一个最容易让人混淆的区别是,Common Lisp 使用空列表 () 表示 false,而 Scheme 的大多数实现(包括所有符合版本 5 标准的实现)都将其视为 true。

4 One of the more confusing differences between Scheme and Common Lisp is that Common Lisp uses the empty list () for false, while most implementations of Scheme (including all that conform to the version 5 standard) treat it as true.

5为了清楚起见,C-3.4.2 节中的图省略了对的内部结构。

5 For clarity, the figures in Section C-3.4.2 elided the internal structure of the pairs.

6回想一下, delay是一种特殊形式,它创建了一个 [备忘录,闭包] 对; force是一个返回备忘录中的值的函数,如果需要的话,先使用闭包来计算它。

6 Recall that delay is a special form that creates a [memo, closure] pair; force is a function that returns the value in the memo, using the closure to calculate it first if necessary.

12

逻辑语言

Logic Languages

详细讨论了函数式语言之后,我们现在来讨论逻辑语言。编程语言设计中的命令式概念和函数式概念的重叠使得我们在文中多次讨论后者。我们较少有机会评论逻辑编程语言的特点。当然,逻辑在数字电路设计中被广泛使用,大多数编程语言都提供逻辑(布尔)类型和运算符。逻辑在语言语义的形式化研究中也被广泛使用,特别是在公理语义中。1在 20 世纪70年代,随着法国艾克斯-马赛大学的 Alain Colmeraurer 和 Philippe Roussel 以及苏格兰爱丁堡大学的 Robert Kowalski 及其同事的工作,研究人员也开始将逻辑推理过程用作一种通用的计算模型。

Having considered functional languages in some detail, we now turn to logic languages. The overlap between imperative and functional concepts in programming language design has led us to discuss the latter at numerous points throughout the text. We have had less occasion to remark on features of logic programming languages. Logic of course is used heavily in the design of digital circuits, and most programming languages provide a logical (Boolean) type and operators. Logic is also heavily used in the formal study of language semantics, specifically in axiomatic semantics.1 In the 1970s, with the work of Alain Colmeraurer and Philippe Roussel of the University of Aix–Marseille in France and Robert Kowalski and associates at the University of Edinburgh in Scotland, researchers also began to employ the process of logical deduction as a general-purpose model of computing.

我们在第 12.1 节中介绍了逻辑编程的基本概念。然后,我们在第 12.2 节中概述了最广泛使用的逻辑语言 Prolog 。我们依次考虑了解析和统一的概念、对列表和算术的支持以及基于搜索的执行模型。在基于井字游戏提供了一个扩展示例之后,我们将转向更高级的主题,即命令式控制流和数据库操作。

We introduce the basic concepts of logic programming in Section 12.1. We then survey the most widely used logic language, Prolog, in Section 12.2. We consider, in turn, the concepts of resolution and unification, support for lists and arithmetic, and the search-based execution model. After presenting an extended example based on the game of tic-tac-toe, we turn to the more advanced topics of imperative control flow and database manipulation.

函数式编程基于 lambda 演算的形式化,而 Prolog 和其他逻辑语言则基于一阶谓词演算。配套站点上的C-12.3 节简要介绍了这种形式化。函数式语言可以充分利用 lambda 演算的功能(至少在内存和其他资源的限制范围内),而逻辑语言则不能充分利用谓词演算的功能。我们将在第 12.4 节中对相关限制进行一般性逻辑编程评估。

Much as functional programming is based on the formalism of lambda calculus, Prolog and other logic languages are based on first-order predicate calculus. A brief introduction to this formalism appears in Section C-12.3 on the companion site. Where functional languages capture the full capabilities of the lambda calculus, however (within the limits, at least, of memory and other resources), logic languages do not capture the full power of predicate calculus. We consider the relevant limitations as part of a general evaluation of logic programming in Section 12.4.

12.1 逻辑编程概念

12.1 Logic Programming Concepts

逻辑编程系统允许程序员陈述一组公理,从中可以证明定理。逻辑程序的用户陈述一个定理或目标,语言实现尝试找到一组公理和推理步骤(包括变量值的选择),它们一起暗示了目标。在现有的几种逻辑语言中,Prolog 是迄今为止使用最广泛的。

Logic programming systems allow the programmer to state a collection of axioms from which theorems can be proven. The user of a logic program states a theorem, or goal, and the language implementation attempts to find a collection of axioms and inference steps (including choices of values for variables) that together imply the goal. Of the several existing logic languages, Prolog is by far the most widely used.

例 12.1

Example 12.1

霍恩条款

Horn clauses

在几乎所有逻辑语言中,公理都以标准形式编写,称为Horn 子句。 Horn 子句由一个2或H和一个由项B i组成的主体组成

In almost all logic languages, axioms are written in a standard form known as a Horn clause. A Horn clause consists of a head,2 or consequent term H, and a body consisting of terms Bi:

12n

HB1,B2,,Bn

si1_e

这个语句的语义是,当B i全部为真时,我们可以推断H也为真。大声朗读时,我们会说“ H,如果B lB 2,…,并且B n 。”霍恩子句可用于捕获大多数(但不是全部)逻辑语句。(我们将在C-12.3 节中返回完整性问题。)■

The semantics of this statement are that when the Bi are all true, we can deduce that H is true as well. When reading aloud, we say “H, if Bl, B2,…, and Bn.” Horn clauses can be used to capture most, but not all, logical statements. (We return to the issue of completeness in Section C-12.3.) ■

例 12.2

Example 12.2

解决

Resolution

为了导出新的语句,逻辑编程系统会通过称为解析的过程将现有语句组合起来,取消同类项。例如,如果我们知道AB蕴含C ,而C蕴含D,我们可以推断出AB蕴含D

In order to derive new statements, a logic programming system combines existing statements, canceling like terms, through a process known as resolution. If we know that A and B imply C, for example, and that C implies D, we can deduce that A and B imply D:

u12-01-9780124104099

一般而言,像ABCD这样的术语不仅可能由常量(“罗切斯特下雨”)组成,也可能由应用于原子变量的谓词组成:rainy(Rochester)rainy(Seattle)rainy ( X )。■

In general, terms like A, B, C, and D may consist not only of constants (“Rochester is rainy”) but also of predicates applied to atoms or to variables: rainy(Rochester), rainy(Seattle), rainy(X). ■

例 12.3

Example 12.3

统一

Unification

在解析过程中,自由变量可以通过与匹配项中的表达式统一来获取值,就像变量在 ML 中获取类型一样(第 7.2.4 节):

During resolution, free variables may acquire values through unification with expressions in matching terms, much as variables acquire types in ML (Section 7.2.4):

u12-02-9780124104099

在下一节中,我们将更详细地讨论 Prolog。我们将在 C-12.3 节中回顾形式逻辑及其与 Prolog 的关系。■

In the following section we consider Prolog in more detail. We return to formal logic, and to its relationship to Prolog, in Section C-12.3. ■

12.2 序言

12.2 Prolog

就像命令式或函数式语言解释器在定义了各种函数和常量的引用环境上下文中评估表达式一样,Prolog 解释器在假定为真的子句(Horn 子句)数据库上下文中运行。3每个子句由项组成,这些项可以是常量、变量或结构。常量可以是原子或数字。结构可以被视为逻辑谓词或数据结构。

Much as an imperative or functional language interpreter evaluates expressions in the context of a referencing environment in which various functions and constants have been defined, a Prolog interpreter runs in the context of a database of clauses (Horn clauses) that are assumed to be true.3 Each clause is composed of terms, which may be constants, variables, or structures. A constant is either an atom or a number. A structure can be thought of as either a logical predicate or a data structure.

例 12.4

Example 12.4

原子、变量、范围和类型

Atoms, variables, scope, and type

Prolog 中的原子类似于 Lisp 中的符号。从词汇上看,原子看起来像以小写字母开头的标识符、“标点符号”序列或带引号的字符串:

Atoms in Prolog are similar to symbols in Lisp. Lexically, an atom looks like an identifier beginning with a lowercase letter, a sequence of “punctuation” characters, or a quoted character string:

我的_Const+“嗨,妈妈”

t0010

数字类似于其他编程语言的整数和浮点常量。变量看起来像以大写字母开头的标识符:

Numbers resemble the integers and floating-point constants of other programming languages. A variable looks like an identifier beginning with an uppercase letter:

我的变量

由于统一,变量可以在运行时实例化为(即可以取)任意值。每个变量的作用域仅限于它出现的子句。没有声明。类型检查是动态的:它仅在程序试图在运行时使用值作为操作数时发生。■

Variables can be instantiated to (i.e., can take on) arbitrary values at run time as a result of unification. The scope of every variable is limited to the clause in which it appears. There are no declarations. Type checking is dynamic: it occurs only when a program attempts to use a value as an operand at run time. ■

例 12.5

Example 12.5

结构和谓词

Structures and predicates

结构由一个称为函子的原子和一个参数列表组成:

Structures consist of an atom called the functor and a list of arguments:

下雨(罗切斯特)

rainy(rochester)

教学(斯科特,cs254)

teaches(scott, cs254)

bin_tree(foo,bin_tree(bar,glarch))

bin_tree(foo, bin_tree(bar, glarch))

Prolog 要求开括号紧跟在函子后面,中间不能有空格。参数可以是任意项:常量、变量或(嵌套)结构。在内部,典型的 Prolog 实现将每个结构表示为类似 Lisp 的cons单元树。从概念上讲,程序员可能更喜欢将某些结构(例如rainy)视为逻辑谓词。我们使用术语“谓词”来指函子和“参数数量”(参数数量)的组合。谓词rainy的参数数量为 1。谓词teaches 的参数数量为 2。■

Prolog requires the opening parenthesis to come immediately after the functor, with no intervening space. Arguments can be arbitrary terms: constants, variables, or (nested) structures. Internally, a typical Prolog implementation will represent each structure as a tree of Lisp-like cons cells. Conceptually, the programmer may prefer to think of certain structures (e.g., rainy) as logical predicates. We use the term “predicate” to refer to the combination of a functor and an “arity” (number of arguments). The predicate rainy has arity 1. The predicate teaches has arity 2. ■

例 12.6

Example 12.6

事实和规则

Facts and rules

Prolog 数据库中的子句可以分为事实规则,每个子句都以句号结尾。事实是没有右侧的 Horn 子句。它看起来像一个术语(蕴涵符号是隐含的):

The clauses in a Prolog database can be classified as facts or rules, each of which ends with a period. A fact is a Horn clause without a right-hand side. It looks like a single term (the implication symbol is implicit):

下雨(罗切斯特)。

rainy(rochester).

规则有右侧:

A rule has a right-hand side:

下雪(X):下雨(X),寒冷(X)。

snowy(X) :- rainy(X), cold(X).

标记 :- 是蕴涵符号;逗号表示“并且”。出现在霍恩子句头部的变量是全称量化的:对于所有X,如果X下雨并且X冷,则X下雪。■

The token :- is the implication symbol; the comma indicates “and.” Variables that appear in the head of a Horn clause are universally quantified: for all X, X is snowy if X is rainy and X is cold. ■

也可以编写左侧为空的子句。这样的子句称为查询目标查询不会出现在 Prolog 程序中。相反,人们会构建一个事实和规则数据库,然后通过向 Prolog 解释器(或编译后的 Prolog 程序)提供要回答的查询(即要证明的目标)来启动执行。

It is also possible to write a clause with an empty left-hand side. Such a clause is called a query, or a goal. Queries do not appear in Prolog programs. Rather, one builds a database of facts and rules and then initiates execution by giving the Prolog interpreter (or the compiled Prolog program) a query to be answered (i.e., a goal to be proven).

例 12.7

Example 12.7

查询

Queries

在 Prolog 的大多数实现中,查询都是使用特殊 ? 版本的蕴涵符号输入的。如果我们输入以下内容:

In most implementations of Prolog, queries are entered with a special ?- version of the implication symbol. If we were to type the following:

下雨(西雅图)。

rainy(seattle).

下雨(罗切斯特)。

rainy(rochester).

?- 有雨(C)。

?- rainy(C).

Prolog 解释器将会回应

the Prolog interpreter would respond with

C=西雅图

C = seattle

当然,C = rochester也是一个有效答案,但 Prolog 会首先找到seattle,因为它在数据库中排在第一位。(对排序的依赖是 Prolog 背离纯逻辑的方式之一;我们将在第12.4 节进一步讨论这个问题。)如果我们想找到所有可能的解决方案,我们可以通过输入分号来要求解释器继续:

Of course, C = rochester would also be a valid answer, but Prolog will find seattle first, because it comes first in the database. (Dependence on ordering is one of the ways in which Prolog departs from pure logic; we discuss this issue further in Section 12.4.) If we want to find all possible solutions, we can ask the interpreter to continue by typing a semicolon:

C = 西雅图;

C = seattle ;

C=罗切斯特。

C = rochester.

如果有其他可能性,解释器就会省略最后一个句号,让我们有机会输入另一个分号。鉴于

If there had been another possibility, the interpreter would have left off the final period and given us the opportunity to type another semicolon. Given

下雨(西雅图)。

rainy(seattle).

下雨(罗切斯特)。

rainy(rochester).

冷(罗切斯特)。

cold(rochester).

下雪(X):下雨(X),寒冷(X)。

snowy(X) :- rainy(X), cold(X).

查询

the query

?- 下雪(C)。

?- snowy(C).

只会得到一个解决方案。■

will yield only one solution. ■

12.2.1 解析与统一

12.2.1 Resolution and Unification

例 12.8

Example 12.8

Prolog 中的解析

Resolution in Prolog

归结原则(Resolution Principle)是 Robinson [ Rob65 ] 提出的,它指出,如果C 1C 2都是 Horn 子句,且C 1的头与C 2的主体中的一个项相匹配,那么我们可以用C 1的主体替换C 2中的项。请考虑以下示例。

The resolution principle, due to Robinson [Rob65], says that if C1 and C2 are Horn clauses and the head of C1 matches one of the terms in the body of C2, then we can replace the term in C2 with the body of C1. Consider the following example.

需要(jane_doe,his201)。

takes(jane_doe, his201).

需要(jane_doe,cs254)。

takes(jane_doe, cs254).

需要(ajit_chandra,art302)。

takes(ajit_chandra, art302).

需要(ajit_chandra,cs254)。

takes(ajit_chandra, cs254).

同学(X,Y):- 需要(X,Z),需要(Y,Z)。

classmates(X, Y) :- takes(X, Z), takes(Y, Z).

如果我们让Xjane_doeZcs254,我们可以用第二个子句的(空)主体替换最后一个子句右侧的第一个项,从而得到新的规则

If we let X be jane_doe and Z be cs254, we can replace the first term on the right-hand side of the last clause with the (empty) body of the second clause, yielding the new rule

同学(jane_doe,Y) :- 需要(Y,cs254)。

classmates(jane_doe, Y) :- takes(Y, cs254).

换句话说,如果Y选修了cs254 ,则Y是jane_doe的同学。■

In other words, Y is a classmate of jane_doe if Y takes cs254. ■

请注意,最后一条规则右侧有一个变量 ( Z ),但该变量未出现在头部。此类变量是存在量化的:对于所有XY,如果存在一个他们都属于的类Z ,则XY是同班同学。

Note that the last rule has a variable (Z) on the right-hand side that does not appear in the head. Such variables are existentially quantified: for all X and Y, X and Y are classmates if there exists a class Z that they both take.

用于将Xjane_doe关联、将Zcs254关联的模式匹配过程称为“统一”。通过统一而赋值的变量称为“实例化”

The pattern-matching process used to associate X with jane_doe and Z with cs254 is known as unification. Variables that are given values as a result of unification are said to be instantiated.

Prolog 的统一规则规定

The unification rules for Prolog state that

 常数仅与自身统一。

 A constant unifies only with itself.

 两个结构统一当且仅当它们具有相同的函子和相同的元数,并且相应的参数递归统一。

 Two structures unify if and only if they have the same functor and the same arity, and the corresponding arguments unify recursively.

 变量与任何事物联合。如果另一事物有值,则该变量被实例化。如果另一事物是未实例化的变量,则这两个变量以这样的方式关联:如果其中一个变量稍后被赋予值,则该值将由两者共享。

 A variable unifies with anything. If the other thing has a value, then the variable is instantiated. If the other thing is an uninstantiated variable, then the two variables are associated in such a way that if either is given a value later, that value will be shared by both.

例 12.9

Example 12.9

Prolog 和 ML 中的统一

Unification in Prolog and ML

Prolog 中的结构统一与 ML 中形式参数和实际参数类型的统一非常相似。例如,类型为int * 'b list的形式参数将与类型为'a * real list的实际参数在 ML 中统一,方法是将'a实例化为int,将'b实例化为real。■

Unification of structures in Prolog is very much akin to ML's unification of the types of formal and actual parameters. A formal parameter of type int * 'b list, for example, will unify with an actual parameter of type 'a * real list in ML by instantiating 'a to int and 'b to real. ■

例 12.10

Example 12.10

平等与统一

Equality and unification

Prolog 中的相等性是根据“可统一性”来定义的。当且仅当 A 和 B 可以统一时,目标 = ( A, B )才会成功。为了方便起见,目标可以写成 A = B;中缀表示法只是语法糖。按照上述规则,我们有

Equality in Prolog is defined in terms of “unifiability.” The goal = (A, B) succeeds if and only if A and B can be unified. For the sake of convenience, the goal may be written as A = B; the infix notation is simply syntactic sugar. In keeping with the rules above, we have

?-a = a。

?- a = a.

正确。% 常数与自身统一

true.       % constant unifies with itself

?-a=b。

?- a = b.

假。% 但不与另一个常数

false.       % but not with another constant

?-foo(a,b)= foo(a,b)。

?- foo(a, b) = foo(a, b).

正确。% 结构递归相同

true.        % structures are recursively identical

?-X=a。

?- X = a.

X = a. % 变量与常数相乘

X = a.        % variable unifies with constant

?-foo(a,b)=foo(X,b)。

?- foo(a, b) = foo(X, b).

X = a. % 参数必须统一

X = a.        % arguments must unify

例 12.11

Example 12.11

无需实例化

Unification without instantiation

两个变量可以不实例化而统一。如果我们输入

It is possible for two variables to be unified without instantiating them. If we type

?- A = B。

?- A = B.

解释器将直接回应

the interpreter will simply respond

A=B。

A = B.

但是,如果我们输入

If, however, we type

?- A = B,A = a,B = Y。

?- A = B, A = a, B = Y.

(在将a绑定到A之前先将AB统一起来)解释器将线性化统一字符串,并明确表示所有三个变量都等于a

(unifying A and B before binding a to A) the interpreter will linearize the string of unifications and make it clear that all three variables are equal to a:

A = B,B = Y,Y = a。

A = B, B = Y, Y = a.

类似地,假设我们给出以下规则:

In a similar vein, suppose we are given the following rules:

需要实验室 (S):-需要 (S,C),有实验室 (C)。

takes_lab(S) :- takes(S, C), has_lab(C).

has_lab(D) :- meets_in(D,R),is_lab(R)。

has_lab(D) :- meets_in(D, R), is_lab(R).

(如果S选修C并且C是一门实验课,则S选修一门实验课。此外,如果DR室上课并且R是实验室,则D也是一门实验课。)尝试解决这些规则将把第二个规则的头部与第一个规则主体中的第二个术语统一起来,从而导致CD统一,即使它们都没有实例化。■

(S takes a lab class if S takes C and C is a lab class. Moreover D is a lab class if D meets in room R and R is a lab.) An attempt to resolve these rules will unify the head of the second with the second term in the body of the first, causing C and D to be unified, even though neither is instantiated. ■

12.2.2 列表

12.2.2 Lists

例 12.12

Example 12.12

Prolog 中的列表符号

List notation in Prolog

和相等性检查一样,列表操作在 Prolog 中也是一种很常见的操作,因此有自己的符号。[a, b, c]结构是.(a, .(b, .(c, [])))的语法糖,其中[]是空列表, . 是内置的类似cons的谓词。除了一些细微的语法差别(圆括号与方括号;逗号与分号)外,ML 或 Lisp 的用户应该很熟悉这种符号。不过,Prolog 还增加了一项额外的便利功能——一个可选的竖线,用于分隔列表的“尾部”。使用这种符号,[a, b, c]可以表示为[a | [b, c]]、[a, b | [c]][a, b, c | []]。当列表的尾部是变量时,竖线符号特别方便:

Like equality checking, list manipulation is a sufficiently common operation in Prolog to warrant its own notation. The construct [a, b, c] is syntactic sugar for the structure .(a, .(b, .(c, []))), where [] is the empty list and . is a built-in cons-like predicate. With minor syntactic differences (parentheses v. brackets; commas v. semicolons), this notation should be familiar to users of ML or Lisp. Prolog adds an extra convenience, however—an optional vertical bar that delimits the “tail” of the list. Using this notation, [a, b, c] could be expressed as [a | [b, c]], [a, b | [c]],or [a, b, c | []]. The vertical-bar notation is particularly handy when the tail of the list is a variable:

成员(X,[X | _)。

member(X, [X | _).

成员(X,[_ | T]):-成员(X,T)。

member(X, [_ | T]) :- member(X, T).

sorted([]). % 空列表已排序

sorted([]).        % empty list is sorted

sorted([_]). % 单例已排序

sorted([_]).       % singleton is sorted

已排序([A,B | T]):-A = <B,已排序([B | T])。

sorted([A, B | T]) :- A =< B, sorted([B | T]).

     % 如果前两个元素按顺序排列,则复合列表已排序,并且

     % compound list is sorted if first two elements are in order and

     % 列表的其余部分(第一个元素之后)已排序

     % remainder of list (after first element) is sorted

这里 =< 是一个对数字进行操作的内置谓词。下划线是变量的占位符,该变量在子句中的其他任何地方都不需要。请注意,[a, b | c]不正确的列表.(a, .(b, c))。标记序列[a | b, c]在语法上无效。■

Here =< is a built-in predicate that operates on numbers. The underscore is a placeholder for a variable that is not needed anywhere else in the clause. Note that [a, b | c] is the improper list .(a, .(b, c)). The sequence of tokens [a | b, c] is syntactically invalid. ■

例 12.13

Example 12.13

函数、谓词和双向规则

Functions, predicates, and two-way rules

Prolog 解析的一个有趣之处在于,它通常不区分“输入”和“输出”参数(有一些例外,例如下一节中描述的is谓词)。因此,给定

One of the interesting things about Prolog resolution is that it does not in general distinguish between “input” and “output” arguments (there are certain exceptions, such as the is predicate described in the following subsection). Thus, given

附加([],A,A)。

append([], A, A).

附加([H | T],A,[H | L]):-附加(T,A,L)。

append([H | T], A, [H | L]) :- append(T, A, L).

我们可以输入

We can type

? - 附加([a,b,c],[d,e],L)。

?- append([a, b, c], [d, e], L).

L = [a,b,c,d,e].

L = [a, b, c, d, e].

? - 附加(X,[d,e],[a,b,c,d,e])。

?- append(X, [d, e], [a, b, c, d, e]).

X = [a,b,c];

X = [a, b, c] ;

错误的。

false.

? - 附加([a,b,c],Y,[a,b,c,d,e])。

?- append([a, b, c], Y, [a, b, c, d, e]).

Y=[d,e].

Y = [d, e].

此示例突出了函数和 Prolog 谓词之间的区别。前者具有清晰的输入(参数)和输出(结果)概念,而后者则没有。在命令式或函数式语言中,我们将函数应用于参数以生成结果。在逻辑语言中,我们搜索谓词为真的值。(并非所有逻辑语言都同样灵活。例如,Mercury 要求程序员指定参数的输入输出模式。这允许编译器生成速度更快的代码。)请注意,当解释器打印对我们的第二个查询的响应时,尚不确定是否存在其他解决方案。只有在我们输入分号后,它才会投入精力来确定没有其他解决方案。■

This example highlights the difference between functions and Prolog predicates. The former have a clear notion of inputs (arguments) and outputs (results); the latter do not. In an imperative or functional language we apply functions to arguments to generate results. In a logic language we search for values for which a predicate is true. (Not all logic languages are equally flexible. Mercury, for example, requires the programmer to specify in or out modes on arguments. These allow the compiler to generate substantially faster code.) Note that when the interpreter prints its response to our second query, it is not yet certain whether additional solutions might exist. Only after we enter a semicolon does it invest the effort to determine that there are none. ■

12.2.3 算术

12.2.3 Arithmetic

例 12.14

Example 12.14

算术和is谓词

Arithmetic and the is predicate

Prolog 中提供了常用的算术运算符,但它们充当谓词的角色,而不是函数。因此+(2, 3)(也可以写成2 + 3)是一个双参数结构,而不是函数调用。特别是,它不会与5统一:

The usual arithmetic operators are available in Prolog, but they play the role of predicates, not of functions. Thus +(2, 3), which may also be written 2 + 3, is a two-argument structure, not a function call. In particular, it will not unify with 5:

?-(2 + 3)= 5。

?- (2 + 3) = 5.

错误的。

false.

为了处理算术,Prolog 提供了一个内置谓词,is,将其第一个参数与第二个参数的算术值统一起来:

To handle arithmetic, Prolog provides a built-in predicate, is, that unifies its first argument with the arithmetic value of its second argument:

?- 是(X,1 + 2)。
X=3。
?—X 是 1+2。
X=3。% 中缀也可以
?- 1+2 等于 4−1。
错误的。% 第一个参数 (1+2) 已经实例化
?- X 是 Y。
错误% 第二个参数(Y)必须已经实例化
?- Y 是 1+2,X 是 Y。
Y = X,X = 3。% Y 在需要之前就被实例化

12.2.4 搜索/执行顺序

12.2.4 Search/Execution Order

那么 Prolog 如何回答查询(满足目标)呢?它需要的是一系列解析步骤,这些步骤将根据数据库中的子句构建目标,或者证明不存在这样的序列。在形式逻辑领域,可以想象两种主要的搜索策略:

So how does Prolog go about answering a query (satisfying a goal)? What it needs is a sequence of resolution steps that will build the goal out of clauses in the database, or a proof that no such sequence exists. In the realm of formal logic, one can imagine two principal search strategies:

 从现有条款开始,向前推进,尝试得出目标。此策略称为正向链接

 Start with existing clauses and work forward, attempting to derive the goal. This strategy is known as forward chaining.

 从目标开始,然后反向推进,试图将其“分解”为一组预先存在的子句。这种策略称为后向链接

 Start with the goal and work backward, attempting to “unresolve” it into a set of preexisting clauses. This strategy is known as backward chaining.

如果现有规则的数量非常多,但事实的数量很少,则前向链接可能比后向链接更快地找到解决方案。然而,在大多数情况下,后向链接效率更高。Prolog 被定义为使用后向链接。

If the number of existing rules is very large, but the number of facts is small, it is possible for forward chaining to discover a solution more quickly than backward chaining. In most circumstances, however, backward chaining turns out to be more efficient. Prolog is defined to use backward chaining.

例 12.15

Example 12.15

搜索树探索

Search tree exploration

因为归结具有结合性和交换性(练习 12.5),所以后向链接定理证明器可以将其搜索范围限制在归结序列中,其中子句右侧的术语与其他子句的头部按照某种特定顺序(例如,从左到右)逐一统一。由此产生的搜索可以用子目标树来描述,如图12.1所示。Prolog 解释器(或程序)首先从左到右探索该树的深度。它从数据库的开头开始,搜索头部可以与顶级目标统一的规则R。然后,它将R主体中的术语视为子目标,并尝试从左到右递归地满足它们。如果在任何时候子目标失败(无法满足),则解释器返回到前一个子目标并尝试以不同的方式满足它(即将其与不同子句的头部统一)。■

Because resolution is associative and commutative (Exercise 12.5), a backward-chaining theorem prover can limit its search to sequences of resolutions in which terms on the right-hand side of a clause are unified with the heads of other clauses one by one in some particular order (e.g., left to right). The resulting search can be described in terms of a tree of subgoals, as shown in Figure 12.1. The Prolog interpreter (or program) explores this tree depth first, from left to right. It starts at the beginning of the database, searching for a rule R whose head can be unified with the top-level goal. It then considers the terms in the body of R as subgoals, and attempts to satisfy them, recursively, left to right. If at any point a subgoal fails (cannot be satisfied), the interpreter returns to the previous subgoal and attempts to satisfy it in a different way (i.e., to unify it with the head of a different clause). ■

f12-01-9780124104099
图 12.1 Prolog 中的回溯搜索。潜在解决方案树由交替的 AND 和 OR 级别组成。AND 级别由规则右侧的子目标组成,所有子目标都必须满足。OR 级别由替代数据库子句组成,其头部将与上面的子目标统一;其中一个必须满足。符号 _C = _X 表示虽然 C 和 X 都未实例化,但它们彼此关联,这样如果其中任何一个将来收到一个值,它将由两者共享。

例 12.16

Example 12.16

回溯和实例化

Backtracking and instantiation

返回先前目标的过程称为回溯。它与 Icon 中生成器的控制流(第 C-6.5.4 节)非常相似。每当为了在搜索树中寻找不同的路径而“撤消”统一操作时,由于该统一而赋予值或相互关联的变量将返回到其未实例化或未关联的状态。例如,在图 12.1中,当我们回溯到rainy(X)子目标时, Xseattle的绑定就被断开。其效果类似于命令式编程语言中实际参数和形式参数之间的绑定断开,只是 Prolog 将绑定表达为统一而不是子程序调用。■

The process of returning to previous goals is known as backtracking. It strongly resembles the control flow of generators in Icon (Section C-6.5.4). Whenever a unification operation is “undone” in order to pursue a different path through the search tree, variables that were given values or associated with one another as a result of that unification are returned to their uninstantiated or unassociated state. In Figure 12.1, for example, the binding of X to seattle is broken when we backtrack to the rainy(X) subgoal. The effect is similar to the breaking of bindings between actual and formal parameters in an imperative programming language, except that Prolog couches the bindings in terms of unification rather than subroutine calls. ■

Prolog 中回溯搜索的空间管理通常遵循C-9.5.3 节中描述的迭代器的单栈实现。每次开始追寻新的子目标G时,解释器都会将一个框架推送到其堆栈上。如果G失败,则从堆栈中弹出该框架,解释器开始回溯。如果G成功,控制权将返回给“调用者”(搜索树中的父级),但G的框架仍保留在堆栈上。后面的子目标将在上方获得空间 这个休眠框架。如果后续回溯导致解释器搜索满足G的其他方法,控制将能够从上次中断的地方恢复。请注意,除非 G 的所有子目标(以及搜索树中其右侧的所有兄弟)也都失败,否则G不会失败,这意味着堆栈中G的框架上方没有任何东西。在解释器的顶层,用户输入的分号被视为最近满足的子目标失败。

Space management for backtracking search in Prolog usually follows the single-stack implementation of iterators described in Section C-9.5.3. The interpreter pushes a frame onto its stack every time it begins to pursue a new subgoal G. If G fails, the frame is popped from the stack and the interpreter begins to backtrack. If G succeeds, control returns to the “caller” (the parent in the search tree), but G's frame remains on the stack. Later subgoals will be given space above this dormant frame. If subsequent backtracking causes the interpreter to search for alternative ways of satisfying G, control will be able to resume where it last left off. Note that G will not fail unless all of its subgoals (and all of its siblings to the right in the search tree) have also failed, implying that there is nothing above G's frame in the stack. At the top level of the interpreter, a semicolon typed by the user is treated the same as failure of the most recently satisfied subgoal.

例 12.17

Example 12.17

规则评估顺序

Order of rule evaluation

子句是有序的,并且解释器从头到尾考虑它们,这意味着 Prolog 程序的结果是确定的和可预测的。事实上,排序和深度优先搜索的结合意味着 Prolog 程序员必须经常考虑顺序以确保递归程序终止。例如,假设我们有一个描述有向无环图的数据库:

The fact that clauses are ordered, and that the interpreter considers them from first to last, means that the results of a Prolog program are deterministic and predictable. In fact, the combination of ordering and depth-first search means that the Prolog programmer must often consider the order to ensure that recursive programs will terminate. Suppose for example that we have a database describing a directed acyclic graph:

边(a,b)。边(b,c)。边(c,d)。
边(d,e)。边(b,e)。边(d,f)。
路径(X,X)。
路径(X,Y):- 边缘(Z,Y),路径(X,Z)。

最后两个子句告诉我们如何确定从节点X到节点Y是否有路径。如果我们要反转最后一个子句右侧项的顺序,那么 Prolog 解释器将先搜索从X可到达的节点Z ,然后再检查从ZY是否有边。该程序仍可运行,但效率会降低。■

The last two clauses tell us how to determine whether there is a path from node X to node Y. If we were to reverse the order of the terms on the right-hand side of the final clause, then the Prolog interpreter would search for a node Z that is reachable from X before checking to see whether there is an edge from Z to Y. The program would still work, but it would not be as efficient. ■

例 12.18

Example 12.18

无限回归

Infinite regression

现在考虑一下如果我们另外颠倒最后两个子句的顺序会发生什么:

Now consider what would happen if in addition we were to reverse the order of the last two clauses:

路径(X,Y):-路径(X,Z),边缘(Z,Y)。

path(X, Y) :- path(X, Z), edge(Z, Y).

路径(X,X)。

path(X, X).

从逻辑角度来看,我们的数据库仍然定义相同的关系。但是,Prolog 解释器将不再能够找到答案。即使是像?- path(a, a) 这样的简单查询也永远不会终止。要了解原因,请考虑图 12.2。解释器首先将path(a, a)与 path(X, Y) 的左侧:-path(X, Z), edge(Z, Y)统一。然后,它考虑右侧的目标,其中第一个目标(path(X, Z))与同一规则的左侧统一,从而导致无限回归。实际上,Prolog 解释器迷失在搜索树的无限分支中,并且永远找不到右侧的有限分支。我们可以通过按广度优先的顺序探索树来避免此问题,但该策略因其成本高昂而被 Prolog 的设计人员拒绝:它可能需要更多的空间,并且不适合基于堆栈的实现。■

From a logical point of view, our database still defines the same relationships. A Prolog interpreter, however, will no longer be able to find answers. Even a simple query like ?- path(a, a) will never terminate. To see why, consider Figure 12.2. The interpreter first unifies path(a, a) with the left-hand side of path(X, Y) :-path(X, Z), edge(Z, Y). It then considers the goals on the right-hand side, the first of which (path(X, Z)), unifies with the left-hand side of the very same rule, leading to an infinite regression. In effect, the Prolog interpreter gets lost in an infinite branch of the search tree, and never discovers finite branches to the right. We could avoid this problem by exploring the tree in breadth-first order, but that strategy was rejected by Prolog's designers because of its expense: it can require substantially more space, and does not lend itself to a stack-based implementation. ■

f12-02-9780124104099
图 12.2 Prolog 中的无限回归。在该图中,即使是像 ?- path(a, a) 这样的简单查询也永远不会终止:解释器永远不会找到平凡分支。

12.2.5 扩展示例:井字游戏

12.2.5 Extended Example: Tic-Tac-Toe

例 12.19

Example 12.19

Prolog 中的井字游戏

Tic-tac-toe in Prolog

在上一小节中,我们看到了 Prolog 数据库中子句的顺序以及右侧项的顺序如何影响

In the previous subsection we saw how the order of clauses in the Prolog database, and the order of terms within a right-hand side, can affect both the efficiency of

边(a,b)。边(b,c)。边(c,d)。

edge(a, b). edge(b, c). edge(c, d).

边缘(d,e)。边缘(b,e)。边缘(d,f)。

edge(d, e). edge(b, e). edge(d, f).

路径(X,Y):-路径(X,Z),边缘(Z,Y)。

path(X, Y) :- path(X, Z), edge(Z, Y).

路径(X,X)。

path(X, X).

Prolog 程序及其终止能力。排序还允许 Prolog 程序员指示某些解决方案是首选的,并且应该在其他“后备”选项之前考虑。例如,考虑在井字游戏中移动的问题。(井字游戏是在 3×3 的方格网格上进行的游戏。两个玩家XO轮流在空方格中放置标记。如果玩家将三个标记水平、垂直或对角线排成一行,则玩家获胜。)

a Prolog program and its ability to terminate. Ordering also allows the Prolog programmer to indicate that certain resolutions are preferred, and should be considered before other, “fallback” options. Consider, for example, the problem of making a move in tic-tac-toe. (Tic-tac-toe is a game played on a 3 × 3 grid of squares. Two players, X and O, take turns placing markers in empty squares. A player wins if he or she places three markers in a row, horizontally, vertically, or diagonally.)

让我们按行主序将方格从 1 编号到 9。此外,让我们使用 Prolog 事实x(n)表示玩家X已在方格n中放置标记,并使用o(m)表示玩家O已在方格m中放置标记。为简单起见,让我们假设计算机是玩家X,并且轮到X移动。我们希望能够发出查询?- move(A),这将使 Prolog 解释器选择一个好的方格A供计算机接下来占据。

Let us number the squares from 1 to 9 in row-major order. Further, let us use the Prolog fact x(n) to indicate that player X has placed a marker in square n, and o(m) to indicate that player O has placed a marker in square m. For simplicity, let us assume that the computer is player X, and that it is X's turn to move. We should like to be able to issue a query ?- move(A) that will cause the Prolog interpreter to choose a good square A for the computer to occupy next.

显然,我们需要能够判断三个给定的方块是否连成一线。一种表达方式是:

Clearly we need to be able to tell whether three given squares lie in a row. One way to express this is:

有序线(1,2,3)。     有序线(4,5,6)。

ordered_line(1, 2, 3).     ordered_line(4, 5, 6).

有序行(7,8,9)。     有序行(1,4,7)。

ordered_line(7, 8, 9).     ordered_line(1, 4, 7).

有序行(2,5,8)。     有序行(3,6,9)。

ordered_line(2, 5, 8).     ordered_line(3, 6, 9).

有序行(1,5,9)。     有序行(3,5,7)。

ordered_line(1, 5, 9).     ordered_line(3, 5, 7).

线(A,B,C):-有序线(A,B,C)。

line(A, B, C) :- ordered_line(A, B, C).

线(A,B,C):-有序线(A,C,B)。

line(A, B, C) :- ordered_line(A, C, B).

线(A,B,C):-有序线(B,A,C)。

line(A, B, C) :- ordered_line(B, A, C).

线(A,B,C):-有序线(B,C,A)。

line(A, B, C) :- ordered_line(B, C, A).

线(A,B,C):-有序线(C,A,B)。

line(A, B, C) :- ordered_line(C, A, B).

线(A,B,C):-有序线(C,B,A)。

line(A, B, C) :- ordered_line(C, B, A).

很容易证明井字游戏没有必胜策略:任何一方都可以逼和。然而,让我们假设我们的程序正在与一个不完美的对手对战。那么我们的任务就是永不失败,并在对手犯错时最大限度地提高我们获胜的机会。以下规则很有效:

It is easy to prove that there is no winning strategy for tic-tac-toe: either player can force a draw. Let us assume, however, that our program is playing against a less-than-perfect opponent. Our task then is never to lose, and to maximize our chances of winning if our opponent makes a mistake. The following rules work well:

移动(A):-好(A),空(A)。

move(A) :- good(A), empty(A).

完整(A):-x(A)。

full(A) :- x(A).

完整(A):-o(A)。

full(A) :- o(A).

空(A):- \ +(满(A))。

empty(A) :- \+(full(A)).

% 战略:

% strategy:

好(A):- 胜利(A)。     好(A):- 阻止胜利(A)。

good(A) :- win(A).     good(A) :- block_win(A).

好(A):-分裂(A)。     好(A):-strong_build(A)。

good(A) :- split(A).     good(A) :- strong_build(A).

好(A):-弱构建(A)。

good(A) :- weak_build(A).

初始规则表明,我们可以通过选择一个好的空方格来满足目标move(A) 。 \+是一个内置谓词,如果其参数(目标)无法证明,则它会成功;我们将在12.2.6 节中进一步讨论它。如果我们无法证明方格n是满的,即如果数据库中既没有x(n),也没有o(n),则它为空。

The initial rule indicates that we can satisfy the goal move(A) by choosing a good, empty square. The \+ is a built-in predicate that succeeds if its argument (a goal) cannot be proven; we discuss it further in Section 12.2.6. Square n is empty if we cannot prove it is full; that is, if neither x(n) nor o(n) is in the database.

战略的关键在于最后五条规则的顺序。我们的第一选择是获胜:

The key to strategy lies in the ordering of the last five rules. Our first choice is to win:

获胜(A):-x(B),x(C),线(A,B,C)。

win(A) :- x(B), x(C), line(A, B, C).

我们的第二个选择是阻止对手获胜:

Our second choice is to prevent our opponent from winning:

block_win(A):-o(B),o(C),线(A,B,C)。

block_win(A) :- o(B), o(C), line(A, B, C).

我们的第三个选择是制造“分棋”——即我们的对手无法阻止我们下一步获胜的局面(见图12.3):

Our third choice is to create a “split”—a situation in which our opponent cannot prevent us from winning on the next move (see Figure 12.3):

f12-03-9780124104099
图 12.3 塔塔棋游戏中的“分裂”。如果 X占据底部中心方格(方格 8),O 的任何后续动作都无法阻止 X 赢得游戏——O 无法同时阻挡 2-5-8 线和 7-8-9 线。

分裂(A):-x(B),x(C),不同(B,C),

split(A) :- x(B), x(C), different(B, C),

 线(A,B,D),线(A,C,E),空(D),空(E)。

 line(A, B, D), line(A, C, E), empty(D), empty(E).

相同(A,A)。

same(A, A).

不同(A,B):- \ +(相同(A,B))。

different(A, B) :- \+(same(A, B)).

这里我们再次依赖于内置谓词\+

Here we have again relied on the built-in predicate \+.

我们的第四个选择是朝着三连胜的方向发展(即,获得两连胜),这样明显的阻挡动作就不会允许我们的对手朝着三连胜的方向发展:

Our fourth choice is to build toward three in a row (i.e., to get two in a row) in such a way that the obvious blocking move won't allow our opponent to build toward three in a row:

strong_build(A) :-x(B),line(A,B,C),empty(C),\+(risky(C))。

strong_build(A) :- x(B), line(A, B, C), empty(C), \+(risky(C)).

有风险(C):-o(D),线(C,D,E),空(E)。

risky(C) :- o(D), line(C, D, E), empty(E).

除此之外,我们的第五种选择是朝着三连胜的方向发展,这样明显的阻挡动作就不会给我们的对手造成分裂:

Barring that, our fifth choice is to build toward three in a row in such a way that the obvious blocking move won't give our opponent a split:

弱构建(A):-x(B),线(A,B,C),空(C),\ +(double_risky(C))。

weak_build(A) :- x(B), line(A, B, C), empty(C), \+(double_risky(C)).

double_risky(C) :-o(D),o(E),不同(D,E),线(C,D,F),

double_risky(C) :- o(D), o(E), different(D, E), line(C, D, F),

 线(C,E,G),空(F),空(G)。

 line(C, E, G), empty(F), empty(G).

如果这些目标都无法满足,我们最终的默认选择是选择一个未占用的方块,并按以下顺序优先考虑中心、角落和侧面:

If none of these goals can be satisfied, our final, default choice is to pick an unoccupied square, giving priority to the center, the corners, and the sides in that order:

好(5)。

good(5).

好(1)。好(3)。好(7)。好(9)。

good(1). good(3). good(7). good(9).

好(2)。好(4)。好(6)。好(8)。

good(2). good(4). good(6). good(8).

12-01-9780124104099检查你的理解

Check your Understanding

1. 逻辑编程的基础数学形式是什么?

1. What mathematical formalism underlies logic programming?

2. 什么是霍恩条款

2. What is a Horn clause?

3. 简述逻辑编程中解析的过程。

3. Briefly describe the process of resolution in logic programming.

4. 什么是合一?为什么它在逻辑编程中很重要?

4. What is a unification? Why is it important in logic programming?

5.  Prolog 中的子句、术语结构是什么?事实、规则查询是什么?

5. What are clauses, terms, and structures in Prolog? What are facts, rules, and queries?

6. 解释 Prolog 在处理算术方面与命令式语言有何不同。

6. Explain how Prolog differs from imperative languages in its handling of arithmetic.

7.描述 前向链接后向链接之间的区别。Prolog 中默认使用哪种链接?

7. Describe the difference between forward chaining and backward chaining. Which is used in Prolog by default?

8. 描述Prolog的搜索策略。讨论回溯和变量的实例化。

8. Describe the Prolog search strategy. Discuss backtracking and the instantiation of variables.

12.2.6 命令式控制流

12.2.6 Imperative Control Flow

我们已经看到,Prolog 中子句和术语的排序非常重要,对效率、终止和备选方案的选择都有影响。除了简单的排序之外,Prolog 还为程序员提供了几个显式的控制流功能。这些功能中最重要的一个被称为cut

We have seen that the ordering of clauses and of terms in Prolog is significant, with ramifications for efficiency, termination, and choice among alternatives. In addition to simple ordering, Prolog provides the programmer with several explicit control-flow features. The most important of these features is known as the cut.

例 12.20

Example 12.20

切工

The cut

切割是一个零参数谓词,写成感叹号:!。作为子目标,它总是成功,但有一个关键的副作用:它要求解释器执行自将父目标与当前规则左侧统一以来所做的任何选择,包括统一本身的选择。例如,回想一下我们对列表成员资格的定义:

The cut is a zero-argument predicate written as an exclamation point: !. As a subgoal it always succeeds, but with a crucial side effect: it commits the interpreter to whatever choices have been made since unifying the parent goal with the left-hand side of the current rule, including the choice of that unification itself. For example, recall our definition of list membership:

成员(X,[X | _])。

member(X, [X | _]).

成员(X,[_ | T]):-成员(X,T)。

member(X, [_ | T]) :- member(X, T).

如果给定原子a在列表L 中出现n次,则目标?-member(a, L)可以成功n次。这些“额外”成功可能并不总是合适的。它们可能导致计算浪费,特别是对于长列表,当member后面跟着一个可能失败的目标时:

If a given atom a appears in list L n times, then the goal ?- member(a, L) can succeed n times. These “extra” successes may not always be appropriate. They can lead to wasted computation, particularly for long lists, when member is followed by a goal that may fail:

prime_candidate(X):-成员(X,候选人),prime(X)。

prime_candidate(X) :- member(X, Candidates), prime(X).

假设prime(X)的计算成本很高。要确定a是否是素数候选者,我们首先检查它是否是候选者列表的成员,然后检查它是否是素数。如果prime(a)失败,Prolog 将回溯并再次尝试满足member(a, Candidates)。如果a多次出现在候选者列表中,则子目标将再次成功,从而导致重新考虑prime(a)子目标,即使该子目标注定会失败。在找到第一个a后,我们可以切断所有进一步的搜索,从而节省大量时间:

Suppose that prime(X) is expensive to compute. To determine whether a is a prime candidate, we first check to see whether it is a member of the Candidates list, and then check to see whether it is prime. If prime(a) fails, Prolog will backtrack and attempt to satisfy member(a, Candidates) again. If a is in the Candidates list more than once, then the subgoal will succeed again, leading to reconsideration of the prime(a) subgoal, even though that subgoal is doomed to fail. We can save substantial time by cutting off all further searches for a after the first is found:

成员(X,[X | _]):-!。

member(X, [X | _]) :- !.

成员(X,[_ | T]):-成员(X,T)。

member(X, [_ | T]) :- member(X, T).

第一条规则右侧的切分表明,如果X是L的头,我们不应该尝试将member(X, L)与第二条规则左侧统一;该切分使我们只遵守第一条规则。■

The cut on the right-hand side of the first rule says that if X is the head of L, we should not attempt to unify member(X, L) with the left-hand side of the second rule; the cut commits us to the first rule. ■

例 12.21

Example 12.21

\+ 及其实现

\+ and its implementation

确保member(X, L)成功不超过一次的另一种方法是在第二个子句中嵌入\+的使用:

An alternative way to ensure that member(X, L) succeeds no more than once is to embed a use of \+ in the second clause:

成员(X,[X | _])。

member(X, [X | _]).

成员(X,[H | T]):-X \= H,成员(X,T)。

member(X, [H | T]) :- X \= H, member(X, T).

这里X \= H表示XH不会合一;即\+(X = H)。(在某些 Prolog 方言中,\+写作not。这个名字暗示了一种可能有些误导的解释;我们将在12.4.3 节中讨论这个问题。)我们新版本的member将显示与以前相同的高级行为,但效率会略低:现在解释器将真正考虑第二条规则,仅在(重新)将XH合一并反转测试的意义后才放弃它。

Here X \= H means X and H will not unify; that is, \+(X = H). (In some Prolog dialects, \+ is written not. This name suggests an interpretation that may be somewhat misleading; we discuss the issue in Section 12.4.3.) Our new version of member will display the same high-level behavior as before, but will be slightly less efficient: now the interpreter will actually consider the second rule, abandoning it only after (re)unifying X with H and reversing the sense of the test.

事实证明\+实际上是通过 cut 和另外两个内置谓词callfail的组合实现的:

It turns out that \+ is actually implemented by a combination of the cut and two other built-in predicates, call and fail:

\+(P) :- call(P), !, 失败。

\+(P) :- call(P), !, fail.

\+(P)。

\+(P).

调用谓词以一个术语作为参数,并尝试将其作为目标(术语是 Prolog 中一等值)。失败谓词始终会失败。■

The call predicate takes a term as argument and attempts to satisfy it as a goal (terms are first-class values in Prolog). The fail predicate always fails. ■

例 12.22

Example 12.22

使用 cut 修剪不需要的答案

Pruning unwanted answers with the cut

原则上,可以将所有 cut 的使用替换为\+的使用— 将 cut 限制在\+的实现中。这样做通常可以使程序更易于阅读。但是,正如我们所见,它通常会降低程序的效率。在某些情况下,明确使用 cut 实际上可以使程序更易于阅读。考虑我们的井字游戏示例。如果我们在程序中输入分号,它将继续从同一棋盘位置生成一系列越来越差的移动,即使我们只想要第一步。我们可以通过使用 cut 来切断对其他步骤的考虑:

In principle, it is possible to replace all uses of the cut with uses of \+ —to confine the cut to the implementation of \+. Doing so often makes a program easier to read. As we have seen, however, it often makes it less efficient. In some cases, explicit use of the cut may actually make a program easier to read. Consider our tic-tac-toe example. If we type semicolons at the program, it will continue to generate a series of increasingly poor moves from the same board position, even though we only want the first move. We can cut off consideration of the others by using the cut:

移动(A):-好(A),空(A),!。

move(A) :- good(A), empty(A), !.

为了使用\+达到同样的效果,我们必须进行更大的手术(练习 12.8)。■

To achieve the same effect with \+ we would have to do more major surgery (Exercise 12.8). ■

例 12.23

Example 12.23

使用剪切进行选择

Using the cut for selection

一般来说,每当我们想要实现if…then…else的效果时,都可以使用 cut :

In general, the cut can be used whenever we want the effect of if… then … else:

语句:- 条件,!,then_part。

statement :- condition, !, then_part.

语句:- else_part。

statement :- else_part.

例 12.24

Example 12.24

循环失败

Looping with fail

fail谓词可以与“生成器”结合使用来实现循环。我们已经看到(在示例 12.13中)如何通过“向后”驱动一组规则来影响生成器。回想一下append的定义:

The fail predicate can be used in conjunction with a “generator” to implement a loop. We have already seen (in Example 12.13) how to effect a generator by driving a set of rules “backward.” Recall our definition of append:

附加([],A,A)。

append([], A, A).

附加([H | T],A,[H | L]):-附加(T,A,L)。

append([H | T], A, [H | L]) :- append(T, A, L).

如果我们使用 write append(A, B, L),其中L已实例化但AB未实例化,则解释器将找到谓词为真的AB。如果回溯迫使它返回,解释器将寻找另一个 ABappend将根据需要生成对。(这与 Icon 的生成器有很大的类比,在C-6.5.4 节中讨论过。)因此,为了列举列表可以分成对的方式,我们可以遵循使用appendfail的用法:

If we use write append(A, B, L), where L is instantiated but A and B are not, the interpreter will find an A and B for which the predicate is true. If backtracking forces it to return, the interpreter will look for another A and B; append will generate pairs on demand. (There is a strong analogy here to the generators of Icon, discussed in Section C-6.5.4.) Thus, to enumerate the ways in which a list can be partitioned into pairs, we can follow a use of append with fail:

打印分区(L):-附加(A,B,L),

print_partitions(L) :- append(A, B, L),

    写入(A),写入(' '),写入(B),nl,

    write(A), write(' '), write(B), nl,

    失败。

    fail.

nl谓词打印换行符。查询print_partitions ( [a, b, c])生成以下输出:

The nl predicate prints a newline character. The query print_partitions([a, b, c]) produces the following output:

[][a,b,c]

[] [a, b, c]

[a][b,c]

[a] [b, c]

[a, b][c] 复制代码

[a, b] [c]

[a,b,c][]

[a, b, c] []

错误的。

false.

如果我们不希望整体谓词失败,我们可以添加最终规则:

If we don't want the overall predicate to fail, we can add a final rule:

打印分区(_)。

print_partitions(_).

假设该规则最后出现,它将在输出出现后成功,并且解释器将以“ true ”结束。■

Assuming this rule appears last, it will succeed after the output has appeared, and the interpreter will finish with “true.” ■

例 12.25

Example 12.25

使用无界生成器进行循环

Looping with an unbounded generator

在某些情况下,我们可能有一个生成器,它产生无界的值序列。例如,下面生成所有自然数:

In some cases, we may have a generator that produces an unbounded sequence of values. The following, for example, generates all of the natural numbers:

自然(l)。

natural(l).

自然(N):-自然(M),N 是 M+l。

natural(N) :- natural(M), N is M+l.

我们可以将此生成器与“测试切割”组合结合使用来迭代前n 个数字:

We can use this generator in conjunction with a “test-cut” combination to iterate over the first n numbers:

我的循环(N):-自然(I),
写(I),nl,% 循环体(nl 打印换行符)
我=N,!。

只要I小于N,相等(统一)谓词就会失败,回溯就会寻找自然的另一种替代方案。但是,如果I = N成功,那么就会执行切割,让我们接受当前(最终)选择的I,并成功终止循环。■

So long as I is less than N, the equality (unification) predicate will fail and backtracking will pursue another alternative for natural. If I = N succeeds, however, then the cut will be executed, committing us to the current (final) choice of I, and successfully terminating the loop. ■

这种编程习惯用法(带有测试终止符的无界生成器)称为生成和测试。与 Scheme 的迭代构造(第 11.3.4 节)一样,它通常与副作用一起使用。显然,I/O 就是其中一种副作用。另一个副作用是修改程序数据库。

This programming idiom—an unbounded generator with a test-cut terminator—is known as generate-and-test. Like the iterative constructs of Scheme (Section 11.3.4), it is generally used in conjunction with side effects. One such side effect, clearly, is I/O. Another is modification of the program database.

Prolog 提供了多种 I/O 功能。除了writenl可以打印到当前输出文件之外,read谓词还可用于从当前输入文件中读取术语。使用getput可以读取和写入单个字符。使用seetell可以将输入和输出重定向到不同的文件。最后,内置谓词consultreconsult可用于从文件中读取数据库子句,因此不必手动将它们输入到解释器中。(有些解释器要这样做,只允许以交互方式输入查询。)

Prolog provides a variety of I/O features. In addition to write and nl, which print to the current output file, the read predicate can be used to read terms from the current input file. Individual characters are read and written with get and put. Input and output can be redirected to different files using see and tell. Finally, the built-in predicates consult and reconsult can be used to read database clauses from a file, so they don't have to be typed into the interpreter by hand. (Some interpreters require this, allowing only queries to be entered interactively.)

例 12.26

Example 12.26

使用get输入字符

Character input with get

谓词get尝试将其参数与输入的下一个可打印字符统一,跳过代码低于 32 的 ASCII 字符。4实际上,它的行为就好像它是根据更简单的谓词get0repeat实现的:

The predicate get attempts to unify its argument with the next printable character of input, skipping over ASCII characters with codes below 32.4 In effect, it behaves as if it were implemented in terms of the simpler predicates get0 and repeat:

获取(X):-重复,获取0(X),X >= 32,!。

get(X) :- repeat, get0(X), X >= 32, !.

get0谓词尝试将其参数与输入的下一个字符统一,而不管值如何,并且与get一样,在回溯期间无法重新满足。相比之下, repeat谓词可以成功任意次数;它的行为就像是用以下一对规则实现的:

The get0 predicate attempts to unify its argument with the single next character of input, regardless of value and, like get, cannot be resatisfied during backtracking. The repeat predicate, by contrast, can succeed an arbitrary number of times; it behaves as if it were implemented with the following pair of rules:

重复。

repeat.

重复:—重复。

repeat :- repeat.

在上述get定义中,回溯将根据需要多次返回repeat以生成可打印字符(ASCII 码至少为 32 的字符)。一般来说,repeat允许我们将任何具有副作用的谓词转换为生成器。■

Within the above definition of get, backtracking will return to repeat as often as needed to produce a printable character (one with ASCII code at least 32). In general, repeat allows us to turn any predicate with side effects into a generator. ■

12.2.7 数据库操作

12.2.7 Database Manipulation

例 12.27

Example 12.27

Prolog 程序作为数据

Prolog programs as data

Prolog 中的子句只是术语的集合,通过内置谓词 :- 和, 连接,它们都可以写成中缀或前缀形式:

Clauses in Prolog are simply collections of terms, connected by the built-in predicates :- and,, both of which can be written in either infix or prefix form:

u12-03-9780124104099

这里前缀逗号两边的单引号是为了将其与分隔谓词参数的逗号区分开来。■

Here the single quotes around the prefix commas serve to distinguish them from the commas that separate the arguments of a predicate. ■

例 12.28

Example 12.28

修改 Prolog 数据库

Modifying the Prolog database

子句和数据库内容的结构特性意味着 Prolog 与 Scheme 一样,是同形的:它可以表示自身。它也可以修改自身。正在运行的 Prolog 程序可以使用内置谓词assert将子句添加到其数据库中,或使用retract将其删除:

The structural nature of clauses and database contents implies that Prolog, like Scheme, is homoiconic: it can represent itself. It can also modify itself. A running Prolog program can add clauses to its database with the built-in predicate assert, or remove them with retract:

?- 下雨(X)。

?- rainy(X).

X = 西雅图;

X = seattle ;

X=罗切斯特。

X = rochester.

?- 断言(下雨(锡拉丘兹))。

?- assert(rainy(syracuse)).

真的。

true.

?- 下雨(X)。

?- rainy(X).

X = 西雅图;

X = seattle ;

X=罗切斯特;

X = rochester ;

X=锡拉丘兹。

X = syracuse.

?- 收回(下雨(罗切斯特))。

?- retract(rainy(rochester)).

真的。

true.

?- 下雨(X)。

?- rainy(X).

X = 西雅图;

X = seattle ;

X=锡拉丘兹。

X = syracuse.

还有一个retractall谓词,它可以从数据库中删除所有匹配的子句。■

There is also a retractall predicate that removes all matching clauses from the database. ■

例 12.29

Example 12.29

井字游戏(完整游戏)

Tic-tac-toe (full game)

图 12.4包含一个完整的井字游戏 Prolog 程序。它使用assertretractallcutfailrepeatwrite来玩整个游戏。使用assert将移动添加到数据库中。在每场游戏开始时使用retractall清除它们。这样,用户可以玩多个游戏而无需重新启动解释器。■

Figure 12.4 contains a complete Prolog program for tic-tac-toe. It uses assert, retractall, the cut, fail, repeat, and write to play an entire game. Moves are added to the database with assert. They are cleared with retractall at the beginning of each game. This way the user can play multiple games without restarting the interpreter. ■

f12-04-9780124104099
图 12.4 Prolog 中的井字游戏程序。

设计与实现

Design & Implementation

12.1 同音语言

12.1 Homoiconic languages

正如我们所指出的,Lisp/Scheme 和 Prolog 都是同像的。一些其他语言,特别是 Snobol、Forth 和 Tcl,也具有此属性。这有什么意义呢?对于大多数程序来说,答案是:意义不大。只要我们编写的程序和其他语言一样,程序和数据看起来一样这一事实就只是令人好奇而已。如果我们对元计算(创建可以创建或操纵其他程序或扩展自身的程序)感兴趣,那么这就会变得更有意义。元计算至少要求我们拥有严格意义上的真正的一流函数,也就是说,我们能够生成行为动态确定的新函数。同像语言可以简化元计算,因为无需在程序或程序扩展的内部(数据结构)和外部(语法)表示之间进行转换。

As we have noted, both Lisp/Scheme and Prolog are homoiconic. A few other languages, notably Snobol, Forth, and Tcl, share this property. What is its significance? For most programs the answer is: not much. So long as we write the sorts of programs that we'd write in other languages, the fact that programs and data look the same is really just a curiosity. It becomes something more if we are interested in metacomputing—the creation of programs that create or manipulate other programs, or that extend themselves. Metacomputing requires, at the least, that we have true first-class functions in the strict sense of the term—that is, that we be able to generate new functions whose behavior is determined dynamically. A homoiconic language can simplify metacomputing by eliminating the need to translate between internal (data structure) and external (syntactic) representations of programs or program extensions.

例 12.30

Example 12.30

谓词

The functor predicate

可以使用内置谓词functorarg=…创建 Prolog 中的单个术语或提取其内容。当且仅当T是具有函子F和元数N的术语时,目标functor(T, F, N)才会成功:

Individual terms in Prolog can be created, or their contents extracted, using the built-in predicates functor, arg, and =… The goal functor(T, F, N) succeeds if and only if T is a term with functor F and arity N:

?- 函子(foo(a,b,c),foo,3)。

?- functor(foo(a, b, c), foo, 3).

真的。

true.

?- 函子(foo(a,b,c),F,N)。

?- functor(foo(a, b, c), F, N).

F = foo,

F = foo,

N=3。

N = 3.

?- 函子(T, foo,3)。

?- functor(T, foo, 3).

T = foo(_G10,_G37,_G24)。

T = foo(_G10, _G37, _G24).

在输出的最后一行中,带有前导下划线的原子是未实例化变量的占位符。■

In the last line of output, the atoms with leading underscores are placeholders for uninstantiated variables. ■

例 12.31

Example 12.31

在运行时创建术语

Creating terms at run time

当且仅当目标arg(N, T, A)的前两个参数( N和T )被实例化、 N是自然数、T是项且AT第 N 个参数时,目标arg(N, T, A ) 才会成功:

The goal arg(N, T, A) succeeds if and only if its first two arguments (N and T) are instantiated, N is a natural number, T is a term, and A is the Nth argument of T:

?-arg(3,foo(a,b,c),A)。

?- arg(3, foo(a, b, c), A).

A=c。

A = c.

结合使用函子参数,我们可以创建一个任意项:

Using functor and arg together, we can create an arbitrary term:

?- 函子(T,foo,3),arg(1,T,a),arg(2,T,b),arg(3,T,c)。

?- functor(T, foo, 3), arg(1, T, a), arg(2, T, b), arg(3, T, c).

T = foo(a,b,c)。

T = foo(a, b, c).

或者,我们可以使用(中缀)=.. 谓词,它将一个术语“等同于”一个列表:

Alternatively, we can use the (infix) =.. predicate, which “equates” a term with a list:

?-T = .. [foo,a,b,c].

?- T =.. [foo, a, b, c].

T = foo(a,b,c)。

T = foo(a, b, c).

?-foo(a,b,c)=..[F,A1,A2,A3]。

?- foo(a, b, c) =.. [F, A1, A2, A3].

F = foo,

F = foo,

A1=a,

A1 = a,

A2=b,

A2 = b,

A3=c。

A3 = c.

注意

Note that

?-foo(a,b,c)= F(A1,A2,A3)。

?- foo(a, b, c) = F(A1, A2, A3).

and

?-F(A1,A2,A3)= foo(a,b,c)。

?- F(A1, A2, A3) = foo(a, b, c).

不起作用:左括号前面的术语必须是原子,而不是变量。■

do not work: the term preceding a left parenthesis must be an atom, not a variable. ■

例 12.32

Example 12.32

追求动态目标

Pursuing a dynamic goal

使用 =.. 和call,程序员可以安排追求(尝试满足)在运行时创建的目标:

Using =.. and call, the programmer can arrange to pursue (attempt to satisfy) a goal created at run time:

param_loop(L,H,F):-自然(I),I> = L,

param_loop(L, H, F) :- natural(I), I >= L,

    G =.. [F, I], 调用(G),

    G =.. [F, I], call(G),

    我=H,!。

    I = H, !.

目标param_loop(5, 10, write)将产生以下输出:

The goal param_loop(5, 10, write) will produce the following output:

5678910

5678910

真的。

true.

如果我们想让数字分行,可以这样写

If we want the numbers on separate lines we can write

?- param_loop(5, 10, writeln)。

?- param_loop(5, 10, writeln).

在哪里

where

writeln(X) :-write(X), nl。

writeln(X) :- write(X), nl.

例 12.33

Example 12.33

自定义数据库浏览

Custom database perusal

综上所述,上述谓词允许 Prolog 程序创建和分解子句,以及在数据库中添加和删除子句。然而,到目前为止,我们用于浏览数据库(即确定其内容)的唯一机制是内置搜索机制。为了允许程序为了以更一般的方式“推理”,Prolog 提供了一个子句谓词,它尝试将其两个参数与数据库中某个现有子句的头部和主体进行匹配:

Taken together, the predicates described above allow a Prolog program to create and decompose clauses, and to add and subtract them from the database. So far, however, the only mechanism we have for perusing the database (i.e., to determine its contents) is the built-in search mechanism. To allow programs to “reason” in more general ways, Prolog provides a clause predicate that attempts to match its two arguments against the head and body of some existing clause in the database:

设计与实现

Design & Implementation

12.2 反射

12.2 Reflection

反射机制允许程序自我推理。虽然没有一种广泛使用的语言是完全反射的,即它可以检查其结构和当前状态的各个方面,但是几种主要语言中出现了重要的反射形式,Prolog 就是其中之一。给定起始目标的函子和元数,子句谓词允许我们在数据库中找到与该目标相关的所有内容。使用子句,我们实际上可以创建一个元循环解释器练习 12.13 ) - Prolog 本身的实现 - 就像我们在 Lisp 中使用evalapply第 11.3.5 节)一样。我们还可以编写使用非标准搜索顺序的求值器(例如,广度优先或前向链接;参见练习 12.14)。Java、C# 和主要脚本语言中出现了其他丰富的反射功能的例子。正如我们将在第 16.3.1 节中看到的那样,它们允许程序检查和推理其完整的类型结构。一些语言(例如 Python)允许程序以文本形式检查其源代码,但这并不像 Prolog 或 Scheme 的同像检查那么强大,后者允许程序直接推理其自身的代码结构。

A reflection mechanism allows a program to reason about itself. While no widely used language is fully reflective, in the sense that it can inspect every aspect of its structure and current state, significant forms of reflection appear in several major languages, Prolog among them. Given the functor and arity of a starting goal, the clause predicate allows us to find everything related to that goal in the database. Using clause, we can in fact create a metacircular interpreter (Exercise 12.13)—an implementation of Prolog in itself—much as we could for Lisp using eval and apply (Section 11.3.5). We can also write evaluators that use nonstandard search orders (e.g., breadth-first or forward chaining; see Exercise 12.14). Other examples of rich reflection facilities appear in Java, C#, and the major scripting languages. As we shall see in Section 16.3.1, these allow a program to inspect and reason about its complete type structure. A few languages (e.g., Python) allow a program to inspect its source code as text, but this is not as powerful as the homoiconic inspection of Prolog or Scheme, which allows a program to reason about its own code structure directly.

?- 子句(snowy(X),B)。

?- clause(snowy(X), B).

B = 下雨(X),寒冷(X)。

B = rainy(X), cold(X).

这里我们发现数据库中只有一条规则,其头部是一个带有函子snowy的单参数项。该规则的主体是连词B = rainy(X), cold(X)。如果有更多这样的子句,我们将有机会通过输入分号来请求它们。Prolog 要求子句的第一个参数充分实例化,以便可以确定其函子和元数。

Here we have discovered that there is a single rule in the database whose head is a single-argument term with functor snowy. The body of that rule is the conjunction B = rainy(X), cold(X). If there had been more such clauses, we would have had the opportuity to ask for them them by entering semicolons. Prolog requires that the first argument to clause be sufficiently instantiated that its functor and arity can be determined.

没有主体(事实)的子句与主体真实匹配:

A clause with no body (a fact) matches the body true:

?- 子句(rainy(rochester),true)。

?- clause(rainy(rochester), true).

真的。

true.

请注意,子句与调用完全不同:它不会尝试满足目标,而只是将其与现有子句进行匹配:

Note that clause is quite different from call: it does not attempt to satisfy a goal, but simply to match it against an existing clause:

?- 子句(snowy(rochester),true)。

?- clause(snowy(rochester), true).

错误的。

false.

各种其他内置谓词也可用于“解构”子句的内容。var谓词采用单个参数;当且仅当其参数为未实例化的变量时,它才会作为目标成功。原子谓词整数谓词当且仅当它们的参数分别为原子和整数时,才会作为目标成功。名称谓词采用两个参数。当且仅当其第一个参数是原子,而第二个参数是该原子字符的 ASCII 码组成的列表时,它才会作为目标成功。

Various other built-in predicates can also be used to “deconstruct” the contents of a clause. The var predicate takes a single argument; it succeeds as a goal if and only if its argument is an uninstantiated variable. The atom and integer predicates succeed as goals if and only if their arguments are atoms and integers, respectively. The name predicate takes two arguments. It succeeds as a goal if and only if its first argument is an atom and its second is a list composed of the ASCII codes for the characters of that atom.

12.3 理论基础

12.3 Theoretical Foundations

例 12.34

Example 12.34

谓词作为数学对象

Predicates as mathematical objects

在数理逻辑中,谓词是将常量(原子)或变量映射到值truefalse的函数。例如,如果 rainy 是谓词,则rainy(Seattle) = truerainy(Tijuana) = false谓词演算提供了用于构造和推理由谓词应用、运算符等)以及量词∀ 和 ∃组成的命题(语句)的符号和推理规则。逻辑编程形式化了对使给定命题为真的变量值的搜索。■

In mathematical logic, a predicate is a function that maps constants (atoms) or variables to the values true and false. If rainy is a predicate, for example, we might have rainy(Seattle) = true and rainy(Tijuana) = false. Predicate calculus provides a notation and inference rules for constructing and reasoning about propositions (statements) composed of predicate applications, operators (and, or, not, etc.), and the quantifiers ∀ and ∃. Logic programming formalizes the search for variable values that will make a given proposition true. ■

12-02-9780124104099 更深入地

IN MORE DEPTH

在传统的逻辑符号中,有许多方法可以陈述给定的命题。逻辑编程建立在子句形式上,它为每个命题提供唯一的表达式。许多(但不是全部)子句形式都可以转换为 Horn 子句的集合,从而翻译成 Prolog。在配套网站上,我们追踪了将任意命题翻译成子句形式所需的步骤。我们还描述了这种形式可以和不能翻译成 Prolog 的情况。

In conventional logical notation there are many ways to state a given proposition. Logic programming is built on clausal form, which provides a unique expression for every proposition. Many though not all clausal forms can be cast as a collection of Horn clauses, and thus translated into Prolog. On the companion site we trace the steps required to translate an arbitrary proposition into clausal form. We also characterize the cases in which this form can and cannot be translated into Prolog.

12.4 逻辑编程的视角

12.4 Logic Programming in Perspective

从理论上讲,逻辑编程是一个非常引人注目的想法:它提出了一种计算模型,我们只需列出未知值的逻辑属性,然后计算机就会找出如何找到它(或告诉我们它不存在)。不幸的是,由于理论和实践原因,现实与这一愿景相差甚远。

In the abstract, logic programming is a very compelling idea: it suggests a model of computing in which we simply list the logical properties of an unknown value, and then the computer figures out how to find it (or tells us it doesn't exist). Unfortunately, reality falls quite a bit short of the vision, for both theoretical and practical reasons.

12.4.1 未涵盖的逻辑部分

12.4.1 Parts of Logic Not Covered

如第 12.3 节所述,霍恩子句并未涵盖一阶谓词演算的全部内容。具体而言,它们不能用于表达子句形式包含具有多个非否定项的析取的语句。我们有时可以在 Prolog 中使用\+谓词来解决这个问题,但语义并不相同(参见第 12.4.3 节)。

As noted in Section 12.3, Horn clauses do not capture all of first-order predicate calculus. In particular, they cannot be used to express statements whose clausal form includes a disjunction with more than one non-negated term. We can sometimes get around this problem in Prolog by using the \+ predicate, but the semantics are not the same (see Section 12.4.3).

12.4.2 执行顺序

12.4.2 Execution Order

例 12.35

Example 12.35

排序速度非常慢

Sorting incredibly slowly

12.2.4 节中,我们看到,必须经常考虑执行顺序才能确保 Prolog 搜索终止。即使对于终止的搜索,简单的代码也可能非常低效。考虑排序问题。自然的声明式L2是L1的排序版本的一种方式是说L2是L1的排列并且L2是排序的:

In Section 12.2.4, we saw that one must often consider execution order to ensure that a Prolog search will terminate. Even for searches that terminate, naive code can be very inefficient. Consider the problem of sorting. A natural declarative way to say that L2 is the sorted version of L1 is to say that L2 is a permutation of L1 and L2 is sorted:

设计与实现

Design & Implementation

12.3 实现逻辑

12.3 Implementing logic

谓词演算是一种比 lambda 演算高级得多的符号。它更加抽象,算法性更低。因此,像 Prolog 这样的语言自然不会提供谓词演算的全部功能,而是包含扩展以使其更具算法性。我们可能有一天会达到编程系统能够从非常高级的声明性规范中发现良好算法的地步,但我们还没有达到那个地步。

Predicate calculus is a significantly higher-level notation than lambda calculus. It is much more abstract—much less algorithmic. It is natural, therefore, that a language like Prolog not provide the full power of predicate calculus, and that it include extensions to make it more algorithmic. We may someday reach the point where programming systems are capable of discovering good algorithms from very high-level declarative specifications, but we are not there yet.

声明性排序(L1,L2):-排列(L1,L2),排序(L2)。

declarative_sort(L1, L2) :- permutation(L1, L2), sorted(L2).

排列([],[])。

permutation([], []).

排列(L,[H | T]):-附加(P,[H | S],L),附加(P,S,W),排列(W,T)。

permutation(L, [H | T]) :- append(P, [H | S], L), append(P, S, W), permutation(W, T).

追加排序谓词在第 12.2.2 节中定义。)不幸的是,Prolog 的默认搜索策略可能需要指数时间才能根据这些规则对列表进行排序:它将生成排列,直到找到一个已排序的排列。■

(The append and sorted predicates are defined in Section 12.2.2.) Unfortunately, Prolog's default search strategy may take exponential time to sort a list based on these rules: it will generate permutations until it finds one that is sorted. ■

虽然逻辑本质上是声明性的,但大多数逻辑语言都以确定的顺序探索可能的解决方案树。 Prolog 提供了各种谓词,包括 cut、failrepeat,以控制执行顺序(第 12.2.6 节)。它还提供了谓词,包括assertretractcall,以在执行期间显式操作其数据库。

While logic is inherently declarative, most logic languages explore the tree of possible resolutions in deterministic order. Prolog provides a variety of predicates, including the cut, fail, and repeat, to control that execution order (Section 12.2.6). It also provides predicates, including assert, retract, and call, to manipulate its database explicitly during execution.

例 12.36

Example 12.36

Prolog中的快速排序

Quicksort in Prolog

为了获得更有效的排序,Prolog 程序员必须采用不太自然的“命令式”定义:

To obtain a more efficient sort, the Prolog programmer must adopt a less natural, “imperative” definition:

快速排序([],[])。

quicksort([], []).

快速排序([A | L1],L2):-分区(A,L1,P1,S1),快速排序(P1,P2),快速排序(S1,S2),附加(P2,[A | S2],L2)。

quicksort([A | L1], L2) :- partition(A, L1, P1, S1), quicksort(P1, P2), quicksort(S1, S2), append(P2, [A | S2], L2).

分区(A,[],[],[])。

partition(A, [], [], []).

分区(A,[H | T],[H | P],S):-A > = H,分区(A,T,P,S)。

partition(A, [H | T], [H | P], S) :- A >= H, partition(A, T, P, S).

分区(A,[H | T],P,[H | S])A =< H,分区(A,T,P,S)。

partition(A, [H | T], P, [H | S]) A =< H, partition(A, T, P, S).

在某些情况下,即使是这种排序也比人们希望的效率要低。例如,当给定一个已经排序的列表时,它需要二次时间,而不是O ( n log n )。快速排序的一个好启发式方法是使用第一个、中间和最后一个元素的中位数对列表进行分区。不幸的是,Prolog 没有提供访问列表中间和最后一个元素的简单方法(它没有数组)。■

Even this sort is less efficient than one might hope in certain cases. When given an already-sorted list, for example, it takes quadratic time, instead of O(n log n). A good heuristic for quicksort is to partition the list using the median of the first, middle, and last elements. Unfortunately, Prolog provides no easy way to access the middle and final elements of a list (it has no arrays). ■

设计与实现

Design & Implementation

12.4 替代搜索策略

12.4 Alternative search strategies

一些逻辑编程方法试图定制运行时搜索策略,以便快速满足目标。例如,达林顿 [ Dar90 ] 描述了一种技术,当中间目标G失败时,我们会尝试找到G中变量的替代实例,使其成功,然后再回溯到先前的目标,看看替代实例是否也适用于它们。这种“失败导向搜索”似乎对某些类型的问题很有效。不幸的是,目前还没有已知的通用技术可以自动发现任何给定问题的最佳算法(甚至只是一个“好”算法)。

Some approaches to logic programming attempt to customize the run-time search strategy in a way that is likely to satisfy goals quickly. Darlington [Dar90], for example, describes a technique in which, when an intermediate goal G fails, we try to find alternative instantiations of the variables in G that will allow it to succeed, before backing up to previous goals and seeing whether the alternative instantiations will work in them as well. This “failure-directed search” seems to work well for certain classes of problems. Unfortunately, no general technique is known that will automatically discover the best algorithm (or even just a “good” one) for any given problem.

正如我们在第 10 章中看到的,区分程序的规范和实现很有用。规范说明了程序要做什么;实现说明了如何做。霍恩子句为规范提供了一种极好的符号。当使用搜索规则进行扩充时(如在 Prolog 中),它们允许用相同的符号来表达实现。

As we saw in Chapter 10, it can be useful to distinguish between the specification of a program and its implementation. The specification says what the program is to do; the implementation says how it is to do it. Horn clauses provide an excellent notation for specifications. When augmented with search rules (as in Prolog) they allow implementations to be expressed in the same notation.

12.4.3 否定和“封闭世界”假设

12.4.3 Negation and the “Closed World” Assumption

例 12.37

Example 12.37

否定即失败

Negation as failure

Horn 子句的集合(例如 Prolog 数据库的事实和规则)构成了假定为真事物的列表。它不包括任何假定为假的事物。这种对纯“正”逻辑的依赖意味着 Prolog 的\+谓词不同于逻辑否定。除非假定数据库包含所有为真的东西(这是封闭世界假设),否则目标\+(T)可以成功,因为我们当前的知识不足以证明T。此外,Prolog 中的否定发生在规则右侧的任何隐式存在量词之外。因此

A collection of Horn clauses, such as the facts and rules of a Prolog database, constitutes a list of things assumed to be true. It does not include any things assumed to be false. This reliance on purely “positive” logic implies that Prolog's \+ predicate is different from logical negation. Unless the database is assumed to contain everything that is true (this is the closed world assumption), the goal \+(T) can succeed simply because our current knowledge is insufficient to prove T. Moreover, negation in Prolog occurs outside any implicit existential quantifiers on the right-hand side of a rule. Thus

?- \+(需要(X,his201))。

?- \+(takes(X, his201)).

其中X未实例化,意味着

where X is uninstantiated, means

? ¬∃ X [取( X , his201)​​]

? ¬∃X [takes(X, his201)]

而不是

rather than

? ∃ X [¬takes( X , his201)​​]

? ∃X[¬takes(X, his201)]

如果我们的数据库表明jane_doe拿走了his201,那么目标takes(X, his201)​​ 可以成功,而\+(takes(X, his201)​​)将失败:

If our database indicates that jane_doe takes his201, then the goal takes(X, his201) can succeed, and \+(takes(X, his201)) will fail:

?- \+(需要(X,his201))。

?- \+(takes(X, his201)).

错误的。

false.

如果我们有办法把否定放在量词里面,我们可能希望有一个能够响应的实现

If we had a way to put the negation inside the quantifier, we might hope for an implementation that would respond

?- \+(需要(X,his201))。

?- \+(takes(X, his201)).

X=ajit_chandra

X = ajit_chandra

甚至

or even

?- \+(需要(X,his201))。

?- \+(takes(X, his201)).

X != 珍妮多伊

X != jane_doe

要完全描述X的值,使得 ¬( X , his201 )​​ 为真,需要对解析树进行完整的探索,而 Prolog 只有在所有目标都失败或反复用分号提示时才会这样做。将某种“建设性否定”纳入逻辑编程的机制是一个活跃的研究课题。■

A complete characterization of the values of X for which ¬takes (X, his201) is true would require a complete exploration of the resolution tree, something that Prolog does only when all goals fail, or when repeatedly prompted with semicolons. Mechanisms to incorporate some sort of “constructive negation” into logic programming are an active topic of research. ■

例 12.38

Example 12.38

否定和实例

Negation and instantiation

值得注意的是,\+在失败方面的定义意味着只要\+成功,变量绑定就会丢失。例如:

It is worth noting that the definition of \+ in terms of failure means that variable bindings are lost whenever \+ succeeds. For example:

?- 需要(X,his201)。

?- takes(X, his201).

X=jane_doe

X = jane_doe

?- \+(需要(X,his201))。

?- \+(takes(X, his201)).

错误的。

false.

?- \+(\+(takes(X,his201)))。

?- \+(\+(takes(X, his201))).

true.     % 没有提供 X 的值

true.    % no value for X provided

当 takes 第一次成功时,X绑定到jane_doe。当内部\+失败时,绑定被破坏。然后当外部\+成功时,将创建一个新的绑定到未实例化的值。Prolog 没有提供通过双重否定将X的绑定拉出的方法。■

When takes first succeeds, X is bound to jane_doe. When the inner \+ fails, the binding is broken. Then when the outer \+ succeeds, a new binding is created to an uninstantiated value. Prolog provides no way to pull the binding of X out through the double negation. ■

12-01-9780124104099检查你的理解

Check Your Understanding

9. 解释一下 Prolog 中 cut (!) 的用途。它和\+有什么关系?

9. Explain the purpose of the cut (!) in Prolog. How does it relate to \+?

10. 描述 Prolog 程序偏离纯逻辑编程模型的三种方式。

10. Describe three ways in which Prolog programs can depart from a pure logic programming model.

11. 描述生成和测试编程习惯用法。

11. Describe the generate-and-test programming idiom.

12. 总结 Prolog 的数据库操作功能。一定要提到assertretractclause

12. Summarize Prolog's facilities for database manipulation. Be sure to mention assert, retract, and clause.

13. 哪些类型的逻辑陈述无法用霍恩子句来表达?

13. What sorts of logical statements cannot be captured in Horn clauses?

14. 什么是封闭世界假设?它给逻辑编程带来了什么问题?

14. What is the closed world assumption? What problems does it cause for logic programming?

12.5 总结和结束语

12.5 Summary and Concluding Remarks

在本章中,我们重点介绍了计算的逻辑模型。命令式程序主要通过迭代和副作用进行计算,函数式程序主要通过将参数代入函数进行计算,而逻辑程序则通过逻辑语句的解析进行计算,由统一变量和项的能力驱动。

In this chapter we have focused on the logic model of computing. Where an imperative program computes principally through iteration and side effects, and a functional program computes principally through substitution of parameters into functions, a logic program computes through the resolution of logical statements, driven by the ability to unify variables and terms.

我们的大部分讨论都是通过对主要逻辑语言 Prolog 的考察进行的,我们用它来说明条款和术语、解析和统一、搜索/执行顺序、列表操作和用于检查和修改逻辑数据库的高阶谓词。

Much of our discussion was driven by an examination of the principal logic language, Prolog, which we used to illustrate clauses and terms, resolution and unification, search/execution order, list manipulation, and high-order predicates for inspection and modification ofthe logic database.

与命令式和函数式编程一样,逻辑编程与构造性证明有关。但是,命令式或函数式程序在某种意义上一种证明(证明能够从输入生成输出),而逻辑程序则是一组公理,计算机试图从中构造证明。命令式和函数式编程分别提供了图灵机和 lambda 演算的全部功能(忽略硬件对算术精度、磁盘和内存空间等的限制),而 Prolog 提供的分解定理证明的通用性则不够充分,这是为了提高时间和空间效率。同时,Prolog 扩展了其正式对应部分,使其具有真正的算术、I/O、命令式控制流和高阶谓词,以便进行自我检查和修改。

Like imperative and functional programming, logic programming is related to constructive proofs. But where an imperative or functional program in some sense is a proof (of the ability to generate outputs from inputs), a logic program is a set of axioms from which the computer attempts to construct a proof. And where imperative and functional programming provide the full power of Turing machines and lambda calculus, respectively (ignoring hardware-imposed limits on arithmetic precision, disk and memory space, etc.), Prolog provides less than the full generality of resolution theorem proving, in the interests of time and space efficiency. At the same time, Prolog extends its formal counterpart with true arithmetic, I/O, imperative control flow, and higher-order predicates for self-inspection and modification.

与 Lisp/Scheme 一样,Prolog 大量使用列表,主要是因为它们可以轻松地逐步构建,而无需将分配和修改状态作为单独的操作。与 Lisp/Scheme 一样(但与 ML 及其后代不同),Prolog 是同形的:程序看起来像普通的数据结构,并且可以即时创建、修改和执行。

Like Lisp/Scheme, Prolog makes heavy use of lists, largely because they can easily be built incrementally, without the need to allocate and then modify state as separate operations. And like Lisp/Scheme (but unlike ML and its descendants), Prolog is homoiconic: programs look like ordinary data structures, and can be created, modified, and executed on the fly.

正如我们在第 1 章中强调的那样,不同的计算模型有不同的吸引力。命令式程序更紧密地反映了底层硬件,并且可以更轻松地“调整”以获得高性能。纯函数式程序避免了副作用的语义复杂性,并且已被证明对于操作符号(非数字)数据特别有用。逻辑程序具有高度声明性的语义并强调统一,非常适合强调关系和搜索的问题。同时,它们不强调控制流可能会导致效率低下。在目前的技术水平下,计算机在处理低级细节(例如,指令调度)的能力上已经超越了人类,但人类在发明好的算法方面仍然更胜一筹。

As we stressed in Chapter 1, different models of computing are appealing in different ways. Imperative programs more closely mirror the underlying hardware, and can more easily be “tweaked” for high performance. Purely functional programs avoid the semantic complexity of side effects, and have proved particularly handy for the manipulation of symbolic (nonnumeric) data. Logic programs, with their highly declarative semantics and their emphasis on unification, are well suited to problems that emphasize relationships and search. At the same time, their de-emphasis of control flow can lead to inefficiency. At the current state of the art, computers have surpassed people in their ability to deal with low-level details (e.g., of instruction scheduling), but people are still better at inventing good algorithms.

正如我们在第 1 章中强调的那样,语言类别之间的界限通常非常模糊。Prolog 的回溯搜索与 Icon 中生成器的执行非常相似。Prolog 中的统一类似于(但比)ML 和 Haskell 的模式匹配功能。(统一也用于 ML 和 Haskell 中的类型检查,以及 C++ 中的模板实例化,但这些都是编译时活动。)

As we also stressed in Chapter 1, the borders between language classes are often very fuzzy. The backtracking search of Prolog strongly resembles the execution of generators in Icon. Unification in Prolog resembles (but is more powerful than) the pattern-matching capabilities of ML and Haskell. (Unification is also used for type checking in ML and Haskell, and for template instantiation in C++, but those are compile-time activities.)

纯函数式或基于逻辑的编程有很多优点。虽然大多数 Scheme 和 Prolog 程序都使用了一些命令式语言特性,但这些特性往往是造成大量程序错误的原因。同时,似乎有些编程任务(例如交互式 I/O)几乎不可能不产生副作用。

There is much to be said for programming in a purely functional or logic-based style. While most Scheme and Prolog programs make some use of imperative language features, those features tend to be responsible for a disproportionate share of program bugs. At the same time, there seem to be programming tasks—interactive I/O, for example—that are almost impossible to accomplish without side effects.

12.6 练习

12.6 Exercises

12.1 从示例 12.17开头的子句开始,使用归结法(如示例 12.3所示)以两种不同的方式表明存在从ae的路径。

12.1 Starting with the clauses at the beginning of Example 12.17, use resolution (as illustrated in Example 12.3) to show, in two different ways, that there is a path from a to e.

12.2 解决Prolog中的练习6.22

12.2 Solve Exercise 6.22 in Prolog.

12.3考虑 图 1.2中的Prolog gcd程序。这个程序能“反向”运行,也能“正向”运行吗?(给定整数dn,能否使用它生成一个整数序列m,使得gcd ( n,m )= d?)解释你的答案。

12.3 Consider the Prolog gcd program in Figure 1.2. Does this program work “backward” as well as forward? (Given integers d and n, can you use it to generate a sequence of integers m such that gcd (n, m) = d?) Explain your answer.

12.4本着 示例 11.20的精神,编写一个 Prolog 程序,利用回溯来模拟确定性有限自动机的执行。

12.4 In the spirit of Example 11.20, write a Prolog program that exploits backtracking to simulate the execution of a nondeterministic finite automaton.

12.5 证明归结是可交换和可结合的。具体来说,如果ABC是 Horn 子句,则证明 ( AB ) = ( BA ) 并且 ( ( AB ) ⊕ C ) = ( A ⊕ ( BC ) ),其中 ⊕ 表示归结。一定要思考由于统一而实例化的变量会发生什么。

12.5 Show that resolution is commutative and associative. Specifically, if A, B, and C are Horn clauses, show that (AB) = (BA) and that ((AB) ⊕ C) = (A ⊕ (BC)), where ⊕ indicates resolution. Be sure to think about what happens to variables that are instantiated as a result of unification.

12.6 示例 12.8中,查询?- classmates(jane_doe, X)将成功三次:两次X = jane_doe,一次X = ajit_chandra。说明如何修改classmates(X, Y)规则,以便学生不被视为自己的同学。

12.6 In Example 12.8, the query ?- classmates(jane_doe, X) will succeed three times: twice with X = jane_doe and once with X = ajit_chandra. Show how to modify the classmates(X, Y) rule so that a student is not considered a classmate of himself or herself.

12.7 修改示例 12.17,使得对于任意已经实例化的XY ,目标path(X, Y)不会成功超过一次,即使从XY有多条路径。

12.7 Modify Example 12.17 so that the goal path(X, Y), for arbitrary already-instantiated X and Y, will succeed no more than once, even if there are multiple paths from X to Y.

12.8 仅使用\+ (无切入),修改第 12.2.5 节中的井字游戏示例,使其从给定的棋盘位置仅生成一个候选走法。您的解决方案与基于切入的解决方案(示例 12.22)相比如何?

12.8 Using only \+ (no cuts), modify the tic-tac-toe example of Section 12.2.5 so it will generate only one candidate move from a given board position. How does your solution compare to the cut-based one (Example 12.22)?

12.9证明 例 12.19中的论断:井字游戏没有必胜策略,即任何一个玩家都可以强行平局。

12.9 Prove the claim, made in Example 12.19, that there is no winning strategy in tic-tac-toe—that either player can force a draw.

12.10证明 例 12.19中的井字游戏策略是最优的(尽可能战胜不完美的对手,否则打平),或者给出一个反例。

12.10 Prove that the tic-tac-toe strategy of Example 12.19 is optimal (wins against an imperfect opponent whenever possible, draws otherwise), or give a counterexample.

12.11 图 12.4中的井字游戏程序开始,画一个有向无环图,其中每个子句都是一个节点,从AB 的弧表示,对于正确性或效率而言,程序中A先于B非常重要。(不要画任何其他弧。)图的任何拓扑排序都应构成程序的同等高效版本。(现有程序是其中之一吗?)

12.11 Starting with the tic-tac-toe program of Figure 12.4, draw a directed acyclic graph in which every clause is a node and an arc from A to B indicates that it is important, either for correctness or efficiency, that A come before B in the program. (Do not draw any other arcs.) Any topological sort of your graph should constitute an equally efficient version of the program. (Is the existing program one of them?)

12.12编写 Prolog 规则来定义 成员谓词的一个版本,该版本将在回溯期间生成列表的所有成员,但不生成重复项。请注意,示例 12.20中的cut 和基于\+ 的版本将还不够;当被要求寻找未实例化的成员时,他们只能找到列表的头部。

12.12 Write Prolog rules to define a version of the member predicate that will generate all members of a list during backtracking, but without generating duplicates. Note that the cut and \+ based versions of Example 12.20 will not suffice; when asked to look for an uninstantiated member, they find only the head of the list.

12.13 使用Prolog 的子句谓词来实现call谓词(假设它不是内置的)。您不需要实现 Prolog 的所有内置谓词;特别是,您可以忽略各种命令式控制流机制和数据库操纵器。通过将数据库作为call的显式参数来扩展您的代码,从而有效地生成一个元循环解释器。

12.13 Use the clause predicate of Prolog to implement the call predicate (pretend that it isn't built in). You needn't implement all of the built-in predicates of Prolog; in particular, you may ignore the various imperative control-flow mechanisms and database manipulators. Extend your code by making the database an explicit argument to call, effectively producing a metacircular interpreter.

12.14 使用Prolog 的子句谓词编写一个谓词call_bfs,尝试广度优先地满足目标。(提示:您需要保留一个尚未实现的子目标队列,每个子目标都由一个捕获回溯替代方案的堆栈表示。)

12.14 Use the clause predicate of Prolog to write a predicate call_bfs that attempts to satisfy goals breadth-first. (Hint: You will want to keep a queue of yet-to-be-pursued subgoals, each of which is represented by a stack that captures backtracking alternatives.)

12.15用 Prolog 编写一个(基于列表的)插入排序算法。在 C 语言中,使用数组的实现如下:void insert_sort(int A[], int N) { int i, j, t; for (i = 1; i < N; i++) { t = A[i]; for (j = i; j > 0; j--) { if (t >= A[j−1]) break; A[j] = A[j−1]; } A[j] = t; } }

 

 

  

  

   

   

    

    

   

   

  

 

12.15 Write a (list-based) insertion sort algorithm in Prolog. Here's what it looks like in C, using arrays:

 void insertion_sort(int A[], int N)

 {

  int i, j, t;

  for (i = 1; i < N; i++) {

   t = A[i];

   for (j = i; j > 0; j--) {

    if (t >= A[j−1]) break;

    A[j] = A[j−1];

   }

   A[j] = t;

  }

 }

12.16 快速排序适用于大型列表,但对于短列表,其开销比插入排序高。用 Prolog 编写一个排序算法,最初使用快速排序,但对于元素不超过 15 个的子列表,切换到插入排序(如上一个练习中所定义)。(提示:您可以在分区操作期间计算元素的数量。)

12.16 Quicksort works well for large lists, but has higher overhead than insertion sort for short lists. Write a sort algorithm in Prolog that uses quicksort initially, but switches to insertion sort (as defined in the previous exercise) for sublists of 15 or fewer elements. (Hint: You can count the number of elements during the partition operation.)

12.17 编写一个 Prolog 排序程序,保证在最坏情况下花费O ( n log n ) 时间。(提示:尝试归并排序;几乎可以在任何算法或数据结构文本中找到描述。)

12.17 Write a Prolog sorting routine that is guaranteed to take O(n log n) time in the worst case. (Hint: Try merge sort; a description can be found in almost any algorithms or data structures text.)

12.18 考虑与 Prolog 解释器的以下交互:?- Y = X,X = foo(X)。Y = foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo (foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo (foo(foo(foo(foo(foo(…这里发生了什么?为什么解释器会陷入无限循环?你能想到任何情况(大概不需要输出)

 

 

 

 

 

 

像这样的结构有什么用处?如果没有用,您能否建议 Prolog 解释器如何实现检查以禁止其创建?这些检查的成本有多高?您认为这些成本是否合理?

12.18 Consider the following interaction with a Prolog interpreter:

 ?- Y = X, X = foo(X).

 Y = foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(

 foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(

 foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(

 foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(foo(

 foo(foo(foo(foo(foo(foo(…

What is going on here? Why does the interpreter fall into an infinite loop? Can you think of any circumstances (presumably not requiring output) in which a structure like this one would be useful? If not, can you suggest how a Prolog interpreter might implement checks to forbid its creation? How expensive would those checks be? Would the cost in your opinion be justified?

12-02-9780124104099 12.19–12.21 更深入。

12.19–12.21  In More Depth.

12.7 探索

12.7 Explorations

12.22 了解 Prolog 和其他逻辑语言的替代搜索策略。正向链接求解器如何工作?智能混合策略的前景如何?

12.22 Learn about alternative search strategies for Prolog and other logic languages. How do forward chaining solvers work? What are the prospects for intelligent hybrid strategies?

12.23  1982 年至 1992 年间,日本政府在逻辑编程方面投入了大量资金。研究由日本国际贸易和工业部 (MITI) 管理的第五代项目。它的目标是什么?实现了什么?没有实现什么?目标和结果与 Prolog 的联系有多紧密?我们今天可以从该项目中学到什么教训?

12.23 Between 1982 and 1992 the Japanese government invested large sums of money in logic programming. Research the Fifth Generation project, administered by the Japanese Ministry of International Trade and Industry (MITI). What were its goals? What was achieved? What was not? How tightly were the goals and outcomes tied to Prolog? What lessons can we learn from the project today?

12.24 提前阅读第 14 章并了解 XSLT,这是一种用于处理以 XML(扩展标记语言,其中最新的网页标准 XHTML 就是一个例子)表示的数据的语言。XSLT 通常被描述为声明式的。它是基于逻辑的吗?它在表达能力、抽象级别和执行效率方面与 Prolog 相比如何?

12.24 Read ahead to Chapter 14 and learn about XSLT, a language used to manipulate data represented in XML, the extended markup language (of which XHTML, the latest standard for web pages, is an example). XSLT is generally described as declarative. Is it logic based? How does it compare to Prolog in expressive power, level of abstraction, and execution efficiency?

12.25 对数据库查询语言 SQL 重复上一个问题(有关介绍,请在您最喜欢的互联网搜索引擎中输入“SQL 教程”)。

12.25 Repeat the previous question for SQL, the database query language (for an introduction, type “SQL tutorial” into your favorite Internet search engine).

12.26 像 Microsoft Excel 这样的电子表格有时被描述为声明式编程。这公平吗?忽略 Visual Basic 宏之类的扩展,定义单元格之间关系的能力是否提供了图灵完备的表达能力?将执行模型与 Prolog 的执行模型进行比较。如何确定单元格的更新顺序?数据可以像在 Prolog 中一样“双向”推送吗?

12.26 Spreadsheets like Microsoft Excel are sometimes characterized as declarative programming. Is this fair? Ignoring extensions like Visual Basic macros, does the ability to define relationships among cells provide Turing complete expressive power? Compare the execution model to that of Prolog. How is the order of update for cells determined? Can data be pushed “both ways,” as they can in Prolog?

12-02-9780124104099 12.27–12.30 更深入。

12.27–12.30  In More Depth.

12.8 书目注释

12.8 Bibliographic Notes

逻辑编程的根源在于自动定理证明。大部分理论基础由 Horn 在 20 世纪 50 年代早期奠定 [ Hor51 ],Robinson 在 20 世纪 60 年代早期奠定 [ Rob65 ]。计算领域的突破发生在20 世纪 70 年代初,法国艾克斯-马赛大学的 Colmeraurer 和 Roussel 与苏格兰爱丁堡大学的 Kowalski 及其同事开发了 Prolog 的初始版本。Robinson [ Rob83 ] 讲述了该语言的早期历史。Lloyd [ Llo87 ]介绍了理论基础。

Logic programming has its roots in automated theorem proving. Much of the theoretical groundwork was laid by Horn in the early 1950s [Hor51], and by Robinson in the early 1960s [Rob65]. The breakthrough for computing came in the early 1970s, when Colmeraurer and Roussel at the University of Aix–Marseille in France and Kowalski and his colleagues at the University of Edinburgh in Scotland developed the initial version of Prolog. The early history of the language is recounted by Robinson [Rob83]. Theoretical foundations are covered by Lloyd [Llo87].

Prolog 最初是为自然语言处理研究而设计的,但很快人们就发现它可以用作通用语言。此后,Prolog 已发展出多个版本。这里描述的是广泛使用的爱丁堡方言。ISO 标准 [ Int95 ] 与之类似。

Prolog was originally intended for research in natural language processing, but it soon became apparent that it could serve as a general-purpose language. Several versions of Prolog have since evolved. The one described here is the widely used Edinburgh dialect. The ISO standard [Int95] is similar.

已经开发了几种其他逻辑语言,但没有一种在受欢迎程度上能与 Prolog 相媲美。OPS5 [ BFKM86 ] 使用了前向链接。Godel [ HL94 ] 包括模块、强类型、更丰富的逻辑运算符以及增强的执行顺序控制。Parlog 是并行 Prolog 方言;我们将在13.4.5 节中简要提到它。Mercury [ SHC96 ] 采用了 ML 系列函数式语言的各种特性,包括静态类型推断、类似 monad 的 I/O、高阶谓词、闭包、柯里化和 lambda 表达式。它是编译型的,而不是解释型的,并且要求程序员指定谓词参数的模式(in、out)

Several other logic languages have been developed, though none rivaled Prolog in popularity. OPS5 [BFKM86] used forward chaining. Godel [HL94] includes modules, strong typing, a richer variety of logical operators, and enhanced control of execution order. Parlog is a parallel Prolog dialect; we will mention it briefly in Section 13.4.5. Mercury [SHC96] adopts a variety of features from ML-family functional languages, including static type inference, monad-like I/O, higher-order predicates, closures, currying, and lambda expressions. It is compiled, rather than interpreted, and requires the programmer to specify modes (in, out) for predicate arguments.

源自 Datalog [ Ull85 ] [ UW08第 4.24.4节] 的数据库查询语言是使用正向链接实现的。CLP(约束逻辑编程)及其变体主要基于 Prolog,但采用更通用的约束满足机制来代替统一 [ JM94 ]。逻辑编程协会的网址为www.cs.nmsu.edu/ALP/

Database query languages stemming from Datalog [Ull85] [UW08, Secs. 4.24.4] are implemented using forward chaining. CLP (Constraint Logic Programming) and its variants are largely based on Prolog, but employ a more general constraint-satisfaction mechanism in place of unification [JM94]. The Association for Logic Programming can be found on-line at www.cs.nmsu.edu/ALP/.


1公理语义将语言中的每个语句或表达式建模为谓词转换器——一种推理规则,它采用一组最初已知为真的条件,并在构造被评估后得出一组保证为真的新条件。形式语义的研究超出了本书的范围。

1 Axiomatic semantics models each statement or expression in the language as a predicate transformer—an inference rule that takes a set of conditions known to be true initially and derives a new set of conditions guaranteed to be true after the construct has been evaluated. The study of formal semantics is beyond the scope of this book.

2请注意,Prolog 中的“head”一词有两种用法:Horn 子句的 head 和列表的 head。两者之间的区别通常从上下文中可以清楚看出。

2 Note that the word “head” is used for two different things in Prolog: the head of a Horn clause and the head of a list. The distinction between these is usually clear from context.

3事实上,对于任何给定的程序,数据库都被认为能够表征所有真实的东西。正如我们将在12.4.3 节中看到的那样,这种封闭世界假设对语言的表达能力施加了某些限制。

3 In fact, for any given program, the database is assumed to characterize everything that is true. As we shall see in Section 12.4.3, this closed world assumption imposes certain limits on the expressiveness of the language.

4令人惊讶的是,ISO Prolog标准并不涵盖Unicode一致性。

4 Surprisingly, the ISO Prolog standard does not cover Unicode conformance.

十三

并发

Concurrency

本文的大部分内容都隐含地集中在顺序程序上:具有单个活动执行上下文的程序。正如我们在第 6 章中看到的,顺序是命令式编程的基础。它在声明式编程中也往往是隐含的,部分原因是实用的函数式和逻辑语言通常包含一些命令式特性,部分原因是人们倾向于开发声明式程序的命令式实现和心理模型(应用顺序减少、带回溯的后向链接),即使语言语义不需要这样的模型。

The bulk of this text has focused, implicitly, on sequential programs: programs with a single active execution context. As we saw in Chapter 6, sequentially is fundamental to imperative programming. It also tends to be implicit in declarative programming, partly because practical functional and logic languages usually include some imperative features, and partly because people tend to develop imperative implementations and mental models of declarative programs (applicative order reduction, backward chaining with backtracking), even when language semantics do not require such a model.

相比之下,如果一个程序可能有多个活动执行上下文(多个“控制线程”),则称该程序是并发的。并发至少有三个重要动机:

By contrast, a program is said to be concurrent if it may have more than one active execution context—more than one “thread of control.” Concurrency has at least three important motivations:

1. 捕捉问题的逻辑结构。许多程序(尤其是服务器和图形应用程序)必须同时跟踪多个基本独立的“任务”。构造此类程序的最简单、最合乎逻辑的方法通常是用单独的控制线程表示每个任务。我们在讨论协程(第9.5 节)和事件(第 9.6 节)时提到过这种“多线程”结构;我们将在13.1.1 节中再次讨论它。

1. To capture the logical structure of a problem. Many programs, particularly servers and graphical applications, must keep track of more than one largely independent “task” at the same time. Often the simplest and most logical way to structure such a program is to represent each task with a separate thread of control. We touched on this “multithreaded” structure when discussing coroutines (Section 9.5) and events (Section 9.6); we will return to it in Section 13.1.1.

2. 利用并行硬件提高速度。多处理器(或处理器中的多个内核)长期以来一直是高端服务器和超级计算机的必备配置,如今已在台式机、笔记本电脑和移动设备中随处可见。为了有效使用这些内核,通常必须在编写(或重写)程序时考虑并发性。

2. To exploit parallel hardware, for speed. Long a staple of high-end servers and supercomputers, multiple processors (or multiple cores within a processor) have become ubiquitous in desktop, laptop, and mobile devices. To use these cores effectively, programs must generally be written (or rewritten) with concurrency in mind.

3. 应对物理分布。在互联网或本地计算机组上运行的应用程序本质上是并发的。许多嵌入式应用程序也是如此:例如,现代汽车的控制系统可能横跨遍布整个车辆的数十个处理器。

3. To cope with physical distribution. Applications that run across the Internet or a more local group of machines are inherently concurrent. So are many embedded applications: the control systems of a modern automobile, for example, may span dozens of processors spread throughout the vehicle.

一般来说,我们使用“并发”这个词来描述任何可能同时进行两个或多个任务(在执行过程中的不可预测点)的系统。根据这个定义,协程不是并发的,因为在任何给定时间,除一个任务外,其他所有任务都会停止在一个已知位置。如果多个任务可以同时在物理上处于活动状态,则并发系统是并行的;这需要多个处理器。这种区别纯粹是实现和性能问题:从语义的角度来看,真正的并行性和在不可预测的时间在任务之间切换的系统的“准并行性”之间没有区别。如果并行系统的处理器与现实世界中彼此物理分离的人或设备相关联,则该系统是分布式的。根据这些定义,“并发”适用于上述所有三个动机。“并行”适用于第二和第三个动机;“分布式”仅适用于第三个。

In general, we use the word concurrent to characterize any system in which two or more tasks may be underway (at an unpredictable point in their execution) at the same time. Under this definition, coroutines are not concurrent, because at any given time, all but one of them is stopped at a well-known place. A concurrent system is parallel if more than one task can be physically active at once; this requires more than one processor. The distinction is purely an implementation and performance issue: from a semantic point of view, there is no difference between true parallelism and the “quasiparallelism” of a system that switches between tasks at unpredictable times. A parallel system is distributed if its processors are associated with people or devices that are physically separated from one another in the real world. Under these definitions, “concurrent” applies to all three motivations above. “Parallel” applies to the second and third; “distributed” applies to only the third.

本章将重点讨论并发性和并行性。自 2005 年左右以来,随着多核处理器的普及,并行性已成为一个紧迫的问题。我们将很少有机会涉及分布式。虽然语言是为分布式计算而设计的,但大多数分布式系统在每个联网处理器上运行单独的程序,并使用消息传递库例程在它们之间进行通信。

We will focus in this chapter on concurrency and parallelism. Parallelism has become a pressing concern since 2005 or so, with the proliferation of multicore processors. We will have less occasion to touch on distribution. While languages have been designed for distributed computing, most distributed systems run separate programs on every networked processor, and use message-passing library routines to communicate among them.

我们首先概述了在现代程序中使用并行性的方式。我们的概述将涉及并发的动机(即使在单处理器上)和竞争概念,这是并发程序复杂性的主要来源。我们还将简要介绍现代多核和多处理器机器的架构特征。在第 13.2 节中,我们将考虑共享内存和消息传递并发模型之间的对比,以及语言和基于库的实现之间的对比。基于协程,我们解释了语言或库如何创建和调度线程。第 13.3 节重点介绍共享内存同步的低级机制。第 13.4 节将讨论扩展到语言级构造。第 13.5 节(主要在配套站点上)讨论了并发的消息传递模型。

We begin our study with an overview of the ways in which parallelism may be used in modern programs. Our overview will touch on the motivation for concurrency (even on uniprocessors) and the concept of races, which are the principal source of complexity in concurrent programs. We will also briefly survey the architectural features of modern multicore and multiprocessor machines. In Section 13.2 we consider the contrast between shared-memory and message-passing models of concurrency, and between language and library-based implementations. Building on coroutines, we explain how a language or library can create and schedule threads. Section 13.3 focuses on low-level mechanisms for shared-memory synchronization. Section 13.4 extends the discussion to language-level constructs. Message-passing models of concurrency are considered in Section 13.5 (mostly on the companion site).

13.1 背景和动机

13.1 Background and Motivation

并发并不是一个新概念。大部分理论基础是在 20 世纪 60 年代奠定的,Algol 68 包含并发编程功能。然而,人们对并发的广泛兴趣是一个相对较新的现象;它部分源于低成本多核和多处理器机器的出现,部分源于图形、多媒体和基于 Web 的应用程序的激增,所有这些都自然地由并发控制线程表示。

Concurrency is not a new idea. Much of the theoretical groundwork was laid in the 1960s, and Algol 68 includes concurrent programming features. Widespread interest in concurrency is a relatively recent phenomenon, however; it stems in part from the availability of low-cost multicore and multiprocessor machines, and in part from the proliferation of graphical, multimedia, and web-based applications, all of which are naturally represented by concurrent threads of control.

并行级别

Levels of Parallelism

并行性出现在现代计算机系统的每个层面。在电路和门级,信号可以同时沿着数千个连接传播,因此并行性相对容易利用。当我们首先向上移动到处理器和核心,然后移动到在其上运行的多层软件时,粒度 并行性(任务的大小和复杂性)在每个级别上都在增加,并且越来越难以确定每个任务应该做什么工作以及任务应该如何协调。

Parallelism arises at every level of a modern computer system. It is comparatively easy to exploit at the level of circuits and gates, where signals can propagate down thousands of connections at once. As we move up first to processors and cores, and then to the many layers of software that run on top of them, the granularity of parallelism—the size and complexity of tasks—increases at every level, and it becomes increasingly difficult to figure out what work should be done by each task and how tasks should coordinate.

40 年来,微架构研究主要致力于寻找更多更好的方法来利用机器语言程序中可用的指令级并行性 (ILP)。正如我们在第 5 章中看到的,深度超标量流水线和积极推测的结合使现代处理器能够跟踪数百条“正在运行”的指令之间的依赖关系,在其中数十条指令上取得进展,并在每个周期内完成几条指令。在世纪之交后不久,很明显已经达到了极限:传统程序中根本没有更多的指令级并行性可用。

For 40 years, microarchitectural research was largely devoted to finding more and better ways to exploit the instruction-level parallelism (ILP) available in machine language programs. As we saw in Chapter 5, the combination of deep, superscalar pipelines and aggressive speculation allows a modern processor to track dependences among hundreds of “in-flight” instructions, make progress on scores of them, and complete several in every cycle. Shortly after the turn of the century, it became apparent that a limit had been reached: there simply wasn't any more instruction-level parallelism available in conventional programs.

在下一个更高的粒度级别,所谓的向量并行性可用于对非常大的数据集的每个元素重复执行操作的程序。从 20 世纪 60 年代末到 90 年代初,利用这种并行性的处理器是超级计算机的主流形式。它们的遗产在主流处理器的向量指令(例如 x86 指令集的 MMX、SSE 和 AVX 扩展)和现代图形处理单元 (GPU) 中得以延续,其峰值性能可以超过典型 CPU(中央处理单元 - 传统核心)的 100 多倍。

At the next higher level of granularity, so-called vector parallelism is available in programs that perform operations repeatedly on every element of a very large data set. Processors designed to exploit this parallelism were the dominant form of supercomputer from the late 1960s through the early 1990s. Their legacy lives on in the vector instructions of mainstream processors (e.g., the MMX, SSE, and AVX extensions to the x86 instruction set), and in modern graphical processing units (GPUs), whose peak performance can exceed that of the typical CPU (central processing unit—a conventional core) by a factor of more than 100.

不幸的是,向量并行只出现在某些类型的程序中。鉴于 ILP 的终结,以及散热对时钟频率的限制(第 C-5.4.4 节),当今的通用计算必须从多核处理器中获得性能改进,而多核处理器需要更粗粒度的线程级并行。因此,转向多核意味着编程性质的根本转变:并行曾经是一个很大程度上不可见的实现细节,现在必须将其明确写入高级程序结构中。

Unfortunately, vector parallelism arises in only certain kinds of programs. Given the end of ILP, and the limits on clock frequency imposed by heat dissipation (Section C-5.4.4), general-purpose computing today must obtain its performance improvements from multicore processors, which require coarser-grain thread-level parallelism. The move to multicore has thus entailed a fundamental shift in the nature of programming: where parallelism was once a largely invisible implementation detail, it must now be written explicitly into high-level program structure.

抽象层次

Levels of Abstraction

在当今的多核机器上,不同类型的程序员需要理解不同细节级别的并发性,并以不同的方式使用它。

On today's multicore machines, different kinds of programmers need to understand concurrency at different levels of detail, and use it in different ways.

最简单、最抽象的情况是使用“黑盒”并行库。例如,排序例程或线性代数包可以并行执行,而其调用者无需了解如何执行。在数据库世界中,以 SQL(结构化查询语言)表达的查询通常也是并行执行的。Microsoft 的 .NET Framework 包含一种语言集成查询机制 (LINQ),允许使用程序数据结构进行数据库样式的查询,同样具有“幕后”并行性。

The simplest, most abstract case arises when using “black box” parallel libraries. A sorting routine or a linear algebra package, for example, may execute in parallel without its caller needing to understand how. In the database world, queries expressed in SQL (Structured Query Language) often execute in parallel as well. Microsoft's .NET Framework includes a Language-Integrated Query mechanism (LINQ) that allows database-style queries to be made of program data structures, again with parallelism “under the hood.”

例 13.1

Example 13.1

C# 中的独立任务

Independent tasks in C#

在稍微不那么抽象的层面上,程序员可能知道某些任务是相互独立的(因为,例如,它们访问不相交的变量)。这样的任务可以安全地并行执行。1例如,在 C# 中,我们可以使用任务并行库编写以下内容:

At a slightly less abstract level, a programmer may know that certain tasks are mutually independent (because, for example, they access disjoint sets of variables). Such tasks can safely execute in parallel.1 In C#, for example, we can write the following using the Task Parallel Library:

Parallel.For(0, 100, i => { A[i] = foo(A[i]); });

Parallel.For(0, 100, i => { A[i] = foo(A[i]); });

Parallel.For的前两个参数是“循环”界限;第三个参数是委托,这里写为 lambda 表达式。假设A是一个 100 个元素的数组,并且foo的调用是真正独立的,则此代码将具有与明显的传统for循环相同的效果,只是它会运行得更快,利用尽可能多的核心(最多 100 个)。■

The first two arguments to Parallel.For are “loop” bounds; the third is a delegate, here written as a lambda expression. Assuming A is a 100-element array, and that the invocations of foo are truly independent, this code will have the same effect as the obvious traditional for loop, except that it will run faster, making use of as many cores as possible (up to 100). ■

例 13.2

Example 13.2

简单的竞争条件

A simple race condition

如果我们的任务不是独立的,如果我们明确同步它们的交互,那么仍然可以并行运行它们。同步通过控制线程操作在时间上交错的方式来消除线程之间的竞争。假设上例中的函数foo从A[i]中减去 1 ,并计算结果为零的次数。我们可以天真地将foo实现为

If our tasks are not independent, it may still be possible to run them in parallel if we explicitly synchronize their interactions. Synchronization serves to eliminate races between threads by controlling the ways in which their actions can interleave in time. Suppose function foo in the previous example subtracts 1 from A[i] and also counts the number of times that the result is zero. Naively we might implement foo as

int零计数;

int zero_count;

公共静态 int foo(int n){

public static int foo(int n) {

 int rtn = n − 1;

 int rtn = n − 1;

 如果 (rtn == 0) zero_count++;

 if (rtn == 0) zero_count++;

 返回 rtn;

 return rtn;

}

}

现在考虑一下当此代码的两个或多个实例同时运行时可能会发生什么:

Consider now what may happen when two or more instances of this code run concurrently:

线程 1主题 21
r1:=零计数r1:=零计数
r1:=r1+1r1:=r1+1
零计数:=r1零计数:=r1

如果指令大致如图所示交错,则两个线程可能加载相同的zero_count值,都可能将其加一,并且都可能将(仅一个更大的)值存储回zero_count。结果可能小于我们的预期。

If the instructions interleave roughly as shown, both threads may load the same value of zero_count, both may increment it by one, and both may store the (only one greater) value back into zero_count. The result may be less than what we expect.

一般而言,只要两个或多个线程“竞相”访问代码中接触某个公共对象的点,就会发生竞争条件,而系统的行为取决于哪个线程先到达那里。在这个特定示例中,线程 1 中的zero_count存储与线程 2 中的加载正在竞争。如果线程 1 先到达那里,我们就会得到“正确”的结果;如果线程 2 先到达那里,我们就不会得到结果。■

In general, a race condition occurs whenever two or more threads are “racing” toward points in the code at which they touch some common object, and the behavior of the system depends on which thread gets there first. In this particular example, the store of zero_count in Thread 1 is racing with the load in Thread 2. If Thread 1 gets there first, we will get the “right” result; if Thread 2 gets there first, we won't. ■

同步的最常见目的是使某些指令序列(称为临界区)看起来具有原子性— — 从所有其他线程的角度来看是“同时”发生的。在我们的示例中,临界区是加载、增量和存储。使序列具有原子性的最常见方法是使用互斥锁,我们在序列的第一条指令之前获取该锁,并在最后一条指令之后释放该锁。我们将在 13.3.1和13.3.5中研究锁。在13.3.2和13.4.4中,我们还将考虑不使用锁实现原子性的机制。

The most common purpose of synchronization is to make some sequence of instructions, known as a critical section, appear to be atomic—to happen “all at once” from the point of view of every other thread. In our example, the critical section is a load, an increment, and a store. The most common way to make the sequence atomic is with a mutual exclusion lock, which we acquire before the first instruction of the sequence and release after the last. We will study locks in Sections 13.3.1 and 13.3.5. In Sections 13.3.2 and 13.4.4 we will also consider mechanisms that achieve atomicity without locks.

在较低的抽象层次上,专业程序员可能需要充分了解硬件和运行时系统才能实现同步机制。本章应该传达这些问题的意识,但在这一层次上的完整处理超出了本书的范围。

At lower levels of abstraction, expert programmers may need to understand hardware and run-time systems in sufficient detail to implement synchronization mechanisms. This chapter should convey a sense of the issues, but a full treatment at this level is beyond the scope of the current text.

13.1.1 多线程程序的情况

13.1.1 The Case for Multithreaded Programs

我们研究并发的第一个动机——捕捉某些应用程序的逻辑结构——在前面的章节中已经出现过几次。在 C-8.7.1 节中,我们注意到交互式 I/O 经常会中断当前程序的执行。例如,在视频游戏中,我们必须处理键击、鼠标或操纵杆移动,同时不断更新屏幕上的图像。构建此类程序的标准方法(如9.6.2 节所述)是在单独的控制线程中执行输入处理程序,该线程与负责更新屏幕的一个或多个线程共存。在9.5 节中,我们考虑了一个屏幕保护程序,该程序使用协同程序将文件系统的“健全性检查”与屏幕上运动图像的更新交错进行。我们还考虑了离散事件模拟,它使用协同程序来表示某个现实世界系统的活动实体。

Our first motivation for concurrency—to capture the logical structure of certain applications—has arisen several times in earlier chapters. In Section C-8.7.1 we noted that interactive I/O must often interrupt the execution of the current program. In a video game, for example, we must handle keystrokes and mouse or joystick motions while continually updating the image on the screen. The standard way to structure such a program, as described in Section 9.6.2, is to execute the input handlers in a separate thread of control, which coexists with one or more threads responsible for updating the screen. In Section 9.5, we considered a screen saver program that used coroutines to interleave “sanity checks” on the file system with updates to a moving picture on the screen. We also considered discrete-event simulation, which uses coroutines to represent the active entities of some real-world system.

离散事件模拟的语义要求事件在固定的时间点以原子方式发生。协程提供了一种自然的实现,因为它们每次只执行一个:只要我们从不在原子操作中切换协程,一切就都很好。然而,在我们的其他示例中——实际上在大多数“自然并发”程序中——不需要协程语义。通过将并发任务分配给线程而不是协程,我们承认如果有多个核心可用,这些任务可以并行进行。我们还将确定哪个线程应该在何时运行的责任从程序员转移到语言实现。作为回报,我们放弃了任何琐碎的原子性概念。

The semantics of discrete-event simulation require that events occur atomically at fixed points in time. Coroutines provide a natural implementation, because they execute one at a time: so long as we never switch coroutines in the middle ofa to-be-atomic operation, all will be well. In our other examples, however— and indeed in most “naturally concurrent” programs—there is no need for coroutine semantics. By assigning concurrent tasks to threads instead of to coroutines, we acknowledge that those tasks can proceed in parallel if more than one core is available. We also move responsibility for figuring out which thread should run when from the programmer to the language implementation. In return, we give up any notion of trivial atomicity.

例 13.3

Example 13.3

多线程网络浏览器

Multithreaded web browser

在基于 Web 的应用程序中,很容易看到对多线程程序的需求。在 Chrome 或 Firefox 等浏览器中(见图13.1),通常有许多不同的线程同时处于活动状态,每个线程在完成其任务之前都可能与远程(可能非常慢)服务器通信多次。当用户点击链接时,浏览器会创建一个线程来请求指定文档。对于除最小页面之外的所有页面,此线程将收到一系列消息“数据包”。当这些数据包开始到达时,线程必须对它们进行格式化,以便在屏幕上显示。格式化任务类似于排版:线程必须访问字体、组合单词并将单词分成几行。对于页面中的许多特殊标签,格式化线程将生成其他线程:每个图像一个线程,如果有背景则一个线程,每个表格一个线程,可能还有更多线程用于处理单独的框架。每个生成的线程将与服务器通信以获取其特定任务所需的信息(例如,图像的内容)。同时,用户可以访问菜单中的项目以创建新的浏览器窗口、编辑书签、更改首选项等,所有这些都与页面元素的呈现“并行”。■

The need for multithreaded programs is easily seen in web-based applications. In a browser such as Chrome or Firefox (see Figure 13.1), there are typically many different threads simultaneously active, each of which is likely to communicate with a remote (and possibly very slow) server several times before completing its task. When the user clicks on a link, the browser creates a thread to request the specified document. For all but the tiniest pages, this thread will then receive a series of message “packets.” As these packets begin to arrive the thread must format them for presentation on the screen. The formatting task is akin to typesetting: the thread must access fonts, assemble words, and break the words into lines. For many special tags within the page, the formatting thread will spawn additional threads: one for each image, one for the background if any, one to format each table, and possibly more to handle separate frames. Each spawned thread will communicate with the server to obtain the information it needs (e.g., the contents of an image) for its particular task. The user, meanwhile, can access items in menus to create new browser windows, edit bookmarks, change preferences, and so on, all in “parallel” with the rendering of page elements. ■

f13-01-9780124104099
图 13.1 假设 Web 浏览器的基于线程的代码。首先近似地,parse_page子例程是 HTML 递归下降解析器的根。然而,在很多情况下,与构造(背景、图像、表格、框架集)识别相关的操作与页面本身的持续解析同时进行。在此示例中,使用 fork 操作创建并发线程响应键盘和鼠标事件,可能会执行另一个线程。

使用多个线程可确保相对较快的操作(例如,显示文本)不会等待较慢的操作(例如,显示大图像)。每当一个线程阻塞(等待消息或 I/O)时,运行时或操作系统都会自动切换核心上的执行以运行不同的线程。在抢占式线程包中,这些上下文切换也会在其他时间发生,以防止任何一个线程占用处理器资源。任何记得早期更连续的浏览器的读者都会欣赏多线程在感知性能和响应能力方面带来的差异,即使在单核机器上也是如此。

The use of many threads ensures that comparatively fast operations (e.g., display of text) do not wait for slow operations (e.g., display of large images). Whenever one thread blocks (waits for a message or I/O), the run-time or operating system will automatically switch execution on the core to run a different thread. In a preemptive thread package, these context switches will occur at other times as well, to prevent any one thread from hogging processor resources. Any reader who remembers early, more sequential browsers will appreciate the difference that multithreading makes in perceived performance and responsiveness, even on a single-core machine.

调度循环替代方案

The Dispatch Loop Alternative

例 13.4

Example 13.4

调度循环网络浏览器

Dispatch loop web browser

如果语言或库不支持线程,浏览器就必须采用更连续的结构,或者将所有延迟事件的处理集中在单个调度循环中(见图13.2)。与调度循环相关的数据结构会跟踪浏览器尚未完成的所有任务。任务的状态可能非常复杂。对于呈现页面的高级任务,状态必须指示已收到哪些数据包以及哪些数据包仍未完成。它还必须识别页面的各个子任务(图像、表格、框架等),以便我们可以找到它们并在用户单击“停止”按钮时恢复它们的状态。

Without language or library support for threads, a browser must either adopt a more sequential structure, or centralize the handling of all delay-inducing events in a single dispatch loop (see Figure 13.2). Data structures associated with the dispatch loop keep track of all the tasks the browser has yet to complete. The state of a task may be quite complicated. For the high-level task of rendering a page, the state must indicate which packets have been received and which are still outstanding. It must also identify the various subtasks of the page (images, tables, frames, etc.) so that we can find them all and reclaim their state if the user clicks on a “stop” button.

f13-02-9780124104099
图 13.2 假设非基于线程的 Web 浏览器的调度循环。continue_task 中的子句必须涵盖任务状态和触发事件的所有可能组合。每个子句中的代码执行其任务的下一个连贯工作单元,在以下情况下返回:(1) 它必须等待事件,(2) 它已经消耗了大量的计算时间,或 (3) 任务已完成。在返回之前,代码分别 (1) 将任务放入字典(由dispatch使用)中,该字典将等待的事件映射到等待它们的任务,(2) 将任务排入ready_tasks中,或 (3) 释放任务。

为了保证良好的交互响应,我们必须确保continue_task的任何子操作都不会花费很长时间来执行。显然,每当我们等待消息时,我们都必须结束当前操作。我们还必须在每次读取文件时结束它,因为磁盘操作很慢。最后,如果任何任务需要计算超过十分之一秒(典型的人类感知阈值),那么我们必须将任务分成几部分,在这些部分之间保存状态并返回到循环顶部。这些考虑意味着循环顶部的条件必须涵盖异步事件的全部范围,并且条件的评估必须与由于计算时间过长而被细分的任何任务的继续执行交错进行。(实际上,我们可能需要一种比简单交错更复杂的机制,以确保输入驱动或计算密集的任务都不会占用超过其份额的资源。)■

To guarantee good interactive response, we must make sure that no subaction of continue_task takes very long to execute. Clearly we must end the current action whenever we wait for a message. We must also end it whenever we read from a file, since disk operations are slow. Finally, if any task needs to compute for longer than about a tenth of a second (the typical human perceptual threshold), then we must divide the task into pieces, between which we save state and return to the top of the loop. These considerations imply that the condition at the top of the loop must cover the full range of asynchronous events, and that evaluations of the condition must be interleaved with continued execution of any tasks that were subdivided due to lengthy computation. (In practice we would probably need a more sophisticated mechanism than simple interleaving to ensure that neither input-driven nor compute-bound tasks hog more than their share of resources.) ■

调度循环的主要问题(除了细分任务和保存状态的复杂性之外)是它隐藏了程序的算法结构。如果不是因为我们必须在每个延迟操作后返回调度循环的顶部,那么每个不同的任务(检索页面、渲染图像、浏览嵌套菜单)都可以用标准控制流机制优雅地描述。实际上,调度循环将程序“彻底颠覆”,使任务的管理变得明确,任务内的控制流变得隐含。由此产生的复杂性类似于我们在尝试使用迭代器对象枚举递归集(见第 6.5.3 节),情况只会更糟。与真正的迭代器一样,线程包将程序“翻转过来”,使任务(线程)的管理隐式化,并使线程内的控制流显式化。

The principal problem with a dispatch loop—beyond the complexity of subdividing tasks and saving state—is that it hides the algorithmic structure of the program. Every distinct task (retrieving a page, rendering an image, walking through nested menus) could be described elegantly with standard control-flow mechanisms, if not for the fact that we must return to the top of the dispatch loop at every delay-inducing operation. In effect, the dispatch loop turns the program “inside out,” making the management of tasks explicit and the control flow within tasks implicit. The resulting complexity is similar to what we encountered when trying to enumerate a recursive set with iterator objects in Section 6.5.3, only worse. Like true iterators, a thread package turns the program “right side out,” making the management of tasks (threads) implicit and the control flow within threads explicit.

13.1.2 多处理器架构

13.1.2 Multiprocessor Architecture

并行计算机硬件种类繁多。分布式系统(我们将其视为在不同机器上运行的不同程序之间的交互)可能大到互联网,也可能小到手机的组件。并行但非分布式系统(我们将其视为在一台机器上运行的单个程序)可能仍然非常庞大。例如,中国的天河二号超级计算机拥有 300 多万个核心,功耗超过 17 兆瓦,占地面积 720 平方米(约五分之一英亩)。

Parallel computer hardware is enormously diverse. A distributed system—one that we think of in terms of interactions among separate programs running on separate machines—maybe as large as the Internet, or as small as the components of a cell phone. A parallel but nondistributed system—one that we think of in terms of a single program running on a single machine—may still be very large. China's Tianhe-2 supercomputer, for example, has more than 3 million cores, consumes over 17 MW of power, and occupies 720 square meters of floor space (about a fifth of an acre).

从历史上看,大多数并行但非分布式机器都是同质的——它们的处理器都是相同的。近年来,许多机器都增加了可编程 GPU,首先是作为单独的处理器,最近则是作为单个处理器芯片的单独部分。虽然 GPU 的内核在内部是同质的,但它们与典型 CPU 的内核非常不同,从而导致全局异构系统。未来的系统可能还会有许多其他类型的内核,每个内核都专用于特定类型的程序或程序组件。

Historically, most parallel but nondistributed machines were homogeneous— their processors were all identical. In recent years, many machines have added programmable GPUs, first as separate processors, and more recently as separate portions of a single processor chip. While the cores of a GPU are internally homogeneous, they are very different from those of the typical CPU, leading to a globally heterogeneous system. Future systems may have cores of many other kinds as well, each specialized to particular kinds of programs or program components.

在理想情况下,编程语言和运行时会在合适的时间将程序片段映射到合适的内核,但这种自动化仍然是一个研究目标。截至 2015 年,想要利用 GPU 的程序员会用 OpenCL 或 CUDA 等专用语言编写适当部分的代码,这些语言强调重复操作而不是向量。然后,在 CPU 上运行的主程序会将生成的“内核”明确发送到 GPU。

In an ideal world, programming languages and runtimes would map program fragments to suitable cores at suitable times, but this sort of automation is still very much a research goal. As of 2015, programmers who want to make use of the GPU write appropriate portions of their code in special-purpose languages like OpenCL or CUDA, which emphasize repetitive operations over vectors. A main program, running on the CPU, then ships the resulting “kernels” to the GPU explicitly.

在本章的剩余部分,我们将集中讨论同构机器的线程级并行性。对于这些机器,许多最重要的架构问题涉及内存系统。在某些机器中,所有物理内存都可以供每个核心访问,并且硬件保证每个写入在任何地方都快速可见。在另一个极端,一些机器将主内存划分给处理器,迫使核心通过某种单独的消息传递机制进行交互。在中间设计中,一些机器以非一致的方式共享内存,只有当两个核心都明确刷新了缓存时,一个核心上的写入才对另一个核心可见。

In the remainder of this chapter, we will concentrate on thread-level parallelism for homogeneous machines. For these, many of the most important architectural questions involve the memory system. In some machines, all of physical memory is accessible to every core, and the hardware guarantees that every write is quickly visible everywhere. At the other extreme, some machines partition main memory among processors, forcing cores to interact through some separate message-passing mechanism. In intermediate designs, some machines share memory in a noncoherent fashion, making writes on one core visible to another only when both have explicitly flushed their caches.

从语言或库实现的角度来看,共享内存和消息传递硬件之间的主要区别在于,消息通常需要连接两端的核心积极参与:一个核心发送,另一个核心接收。在共享内存机器上,一个核心可以读取和写入远程内存,而无需任何其他核心的协助。

From the point of view of language or library implementation, the principal distinction between shared-memory and message-passing hardware is that messages typically require the active participation of cores at both ends of the connection: one to send, the other to receive. On a shared-memory machine, a core can read and write remote memory without any other core's assistance.

在小型机器(比如 2-4 个处理器)上,主内存可能是均匀的——与所有处理器的距离相等。在较大的机器(甚至在一些非常在小型机器中,内存可能不均匀——每个库可能在物理上与特定处理器或小组处理器相邻。然后,任何处理器中的核心都可以访问任何其他处理器的内存,但本地内存速度更快。当然,假设所有内存都被缓存,差异只会出现在缓存未命中上,此时本地内存的惩罚较低。

On small machines (2–4 processors, say), main memory may be uniform— equally distant from all processors. On larger machines (and even on some very small machines), memory may be nonuniform instead—each bank may be physically adjacent to a particular processor or small group of processors. Cores in any processor can then access the memory of any other, but local memory is faster. Assuming all memory is cached, of course, the difference appears only on cache misses, where the penalty for local memory is lower.

记忆连贯性

Memory Coherence

例 13.5

Example 13.5

缓存一致性问题

The cache coherence problem

正如非一致性内存概念所暗示的,缓存为共享内存机器带来了一个严重的问题:除非我们采取特殊措施,否则缓存了特定内存位置的内核可能会运行任意长时间而看不到其他内核对该位置所做的更改。这个问题(如何使缓存的内存位置副本彼此保持一致)称为一致性问题参见图 13.3)。在基于总线的简单机器上,这个问题相对容易解决:通信介质的广播性质允许缓存控制器窃听(窥探)其他内核的内存流量。当内核需要写入缓存行时,它会请求独占副本,并等待其他内核使其副本无效。在总线上,等待是微不足道的,消息的自然顺序决定了在近乎同时的请求中谁会获胜。在失效之后尝试访问某行的内核必须返回内存(或另一个内核的缓存)以获取最新副本。■

As suggested by the notion of noncoherent memory, caches introduce a serious problem for shared-memory machines: unless we do something special, a core that has cached a particular memory location may run for an arbitrarily long time without seeing changes that have been made to that location by other cores. This problem—how to keep cached copies of a memory location consistent with one another—is known as the coherence problem (see Figure 13.3). On a simple bus-based machine, the problem is relatively easy to solve: the broadcast nature of the communication medium allows cache controllers to eavesdrop (snoop) on the memory traffic of other cores. When a core needs to write a cache line, it requests an exclusive copy, and waits for other cores to invalidate their copies. On a bus the waiting is trivial, and the natural ordering of messages determines who wins in the event of near-simultaneous requests. Cores that try to access a line in the wake of invalidation must go back to memory (or to another core's cache) to obtain an up-to-date copy. ■

f13-03-9780124104099
图 13.3 共享内存多核和多处理器计算机的缓存一致性问题。这里,核心AB都从内存中读取了变量X。作为副作用,在每个核心的缓存中创建了X的副本。如果A现在将X更改为 4,而B再次读取X,我们如何确保结果是 4 而不是仍然缓存的 3?同样,如果Z将X读入其缓存,我们如何确保它从A的缓存中获取 4,而不是从内存中获取陈旧的 3?

设计与实现

Design & Implementation

13.1 处理器到底是什么?

13.1 What, exactly, is a processor?

从 1975 年到 2005 年左右,处理器通常一次只运行一个线程,并占用一个完整的芯片。如今,大多数供应商仍使用“处理器”一词来指代“执行计算”的物理设备,其引脚将其连接到计算机的其余部分,但内部结构要复杂得多:物理封装内可能有多个芯片,每个芯片可能有多个内核(在之前的硬件版本中,每个内核都被称为“处理器”),每个内核可能有多个硬件线程(独立的寄存器集,允许内核的管道运行来自多个软件线程的混合指令)。现代处理器还可能包括数兆字节的片上缓存,这些缓存组织成多个级别,并以复杂的方式在内核之间物理分布和共享。处理器可能越来越多地集成片上内存控制器、网络接口、图形处理单元或其他以前的“外围”组件,这使得继续使用“处理器”一词存在问题,但并不罕见。

From roughly 1975 to 2005, a processor typically ran only one thread at a time, and occupied one full chip. Today, most vendors still use the term “processor” to refer to the physical device that “does the computing,” and whose pins connect it to the rest of the computer, but the internal structure is much more complicated: there may be more than one chip inside the physical package, each chip may have multiple cores (each of which would have been called a “processor” in previous hardware generations), and each core may have multiple hardware threads (independent register sets, which allow the core's pipeline(s) to run a mix of instructions drawn from multiple software threads). A modern processor may also include many megabytes of on-chip cache, organized into multiple levels, and physically distributed and shared among the cores in complicated ways. Increasingly, processors may incorporate on-chip memory controllers, network interfaces, graphical processing units, or other formerly “peripheral” components, making continued use of the term “processor” problematic but no less common.

从软件角度来看,好消息是操作系统和编程语言通常将每个并发活动建模为一个线程,无论它是否与其他线程共享核心、芯片或封装。我们将在本章的其余大部分内容中遵循这一惯例,忽略底层硬件的复杂性。当我们需要引用线程运行的硬件时,我们通常会将其称为“核心”。坏消息是,“一切都只是线程”的计算模型隐藏了对理解和提高性能至关重要的细节。未来的芯片可能会包含越来越多的异构核心和复杂的片上网络。为了有效地使用这些芯片,语言实现将需要变得更加复杂,以便将线程调度到底层硬件上。应用程序程序员需要看到多少任务仍有待确定。

From a software perspective, the good news is that operating systems and programming languages generally model every concurrent activity as a thread, regardless of whether it shares a core, a chip, or a package with other threads. We will follow this convention for most of the rest of this chapter, ignoring the complexity of the underlying hardware. When we need to refer to the hardware on which a thread runs, we will usually call it a “core.” The bad news is that a model of computing in which “everything is just a thread” hides details that are crucial to understanding and improving performance. Future chips are likely to include ever larger numbers of heterogeneous cores and complex on-chip networks. To use these chips effectively, language implementations will need to become much more sophisticated about scheduling threads onto the underlying hardware. How much of the task will need to be visible to the application programmer remains to be determined.

基于总线的缓存一致性算法现在是大多数商用微处理器的标准内置部分。在大型机器上,由于缺少广播总线,缓存一致性成为一个更加困难的问题;虽然有商用实现,但它们既复杂又昂贵。无论是在小型机器还是大型机器上,一致性都不是即时的(通知传播需要时间),这意味着我们必须从不同的处理器的角度考虑对不同位置的更新发生的顺序。确保一致性是一个令人惊讶的难题;我们将在第 13.3.3 节中再次讨论它。

Bus-based cache coherence algorithms are now a standard, built-in part of most commercial microprocessors. On large machines, the lack of a broadcast bus makes cache coherence a significantly more difficult problem; commercial implementations are available, but they are complex and expensive. On both small and large machines, the fact that coherence is not instantaneous (it takes time for notifications to propagate) means that we must consider the order in which updates to different locations appear to occur from the point of view of different processors. Ensuring a consistent view is a surprisingly difficult problem; we will return to it in Section 13.3.3.

截至 2015 年,每个主要指令集架构都有多核版本,包括 ARM、x86、Power、SPARC、x86-64 和 IA-64 (Itanium)。数十家制造商提供基于这些架构构建的小型缓存一致性多处理器。多家制造商提供更大型的缓存一致性共享内存多处理器,包括 Oracle、HP、IBM 和 SGI。

As of 2015, there are multicore versions of every major instruction set architecture, including ARM, x86, Power, SPARC, x86-64, and IA-64 (Itanium). Small, cache-coherent multiprocessors built from these are available from dozens of manufacturers. Larger, cache-coherent shared-memory multiprocessors are available from several manufacturers, including Oracle, HP, IBM, and SGI.

超级计算机

Supercomputers

尽管与计算机行业的其他领域相比,超级计算在财务上相形见绌,但它在计算机发展中一直发挥着不成比例的作用技术和人类知识的进步。超级计算机随着时间的推移发生了巨大变化,并且继续以极快的速度发展。然而,它们一直都是并行机器。

Though dwarfed financially by the rest of the computer industry, supercomputing has always played a disproportionate role in the development of computer technology and the advancement of human knowledge. Supercomputers have changed dramatically over time, and they continue to evolve at a very rapid pace. They have always, however, been parallel machines.

由于缓存一致性的复杂性,很难构建大型共享内存机器。SGI 销售的机器最多有 256 个处理器(2048 个内核)。Cray 制造的共享内存机器甚至更大,但没有缓存远程位置的能力。然而,在大多数情况下,20 世纪 60 年代至 80 年代的矢量超级计算机并没有被大型多处理器取代,而是被少量的小型多处理器或大量商用(主流)处理器取代,这些处理器通过定制的高性能网络连接。随着网络技术“渗透”到更广泛的市场,这些机器又让位于由商用多核处理器和商用网络(千兆以太网或 Infiniband)组成的集群。截至 2015 年,集群已开始主宰从中等服务器场到除最快超级计算机站点之外的所有领域。谷歌、亚马逊或 Facebook 等大型在线服务通常由拥有数万或数十万个核心的集群支持(就谷歌而言,可能有数百万个)。

Because of the complexity of cache coherence, it is difficult to build large shared-memory machines. SGI sells machines with as many as 256 processors (2048 cores). Cray builds even larger shared-memory machines, but without the ability to cache remote locations. For the most part, however, the vector supercomputers of the 1960s–80s were displaced not by large multiprocessors, but by modest numbers of smaller multiprocessors or by very large numbers of commodity (mainstream) processors, connected by custom high-performance networks. As network technology “trickled down” into the broader market, these machines in turn gave way to clusters composed of both commodity multicore processors and commodity networks (Gigabit Ethernet or Infiniband). As of 2015, clusters have come to dominate everything from modest server farms up to all but the very fastest supercomputer sites. Large-scale on-line services like Google, Amazon, or Facebook are typically backed by clusters with tens or hundreds of thousands of cores (in Google's case, probably millions).

当今最快的计算机由特殊的高密度多核芯片构成,每核运行功耗较低。天河二号(截至 2015 年 6 月,世界上最快的计算机)采用 2:3 混合的英特尔 12 核 Ivy Bridge 和 61 核 Phi 处理器,每核功耗分别为 10 W 和 5 W。鉴于目前的趋势,未来的计算机(无论是高端计算机还是商用计算机)似乎都将越来越密集,越来越异构。

Today's fastest machines are constructed from special high-density multicore chips with low per-core operating power. The Tianhe-2 (the fastest machine in the world as of June 2015) uses a 2:3 mix of Intel 12-core Ivy Bridge and 61-core Phi processors, at 10 W and 5 W per core, respectively. Given current trends, it seems likely that future machines, both high-end and commodity, will be increasingly dense and increasingly heterogeneous.

从编程语言的角度来看,超级计算的特殊挑战在于适应不一致的访问时间,以及(在大多数情况下)整个机器缺乏对共享内存的硬件支持。当今的超级计算机大多使用消息传递库(尤其是 MPI)以及在本地和远程内存访问之间有明显语法区别的语言和库进行编程。

From a programming language perspective, the special challenge of supercomputing is to accommodate nonuniform access times and (in most cases) the lack of hardware support for shared memory across the full machine. Today's supercomputers are programmed mostly with message-passing libraries (MPI in particular) and with languages and libraries in which there is a clear syntactic distinction between local and remote memory access.

13-01-9780124104099检查你的理解

Check Your Understanding

1.解释 并发、并行分布式之间的区别。

1. Explain the distinctions among concurrent, parallel, and distributed.

2. 解释一下并发的动机。为什么人们要编写并发程序?近年来人们对并发的兴趣日益浓厚,原因何在?

2. Explain the motivation for concurrency. Why do people write concurrent programs? What accounts for the increased interest in concurrency in recent years?

3. 描述现代系统中并行性的实现级别,以及程序员可能考虑并行性的抽象级别。

3. Describe the implementation levels at which parallelism appears in modern systems, and the levels of abstraction at which it may be considered by the programmer.

4. 什么是竞争条件?什么是同步

4. What is a race condition? What is synchronization?

5. 什么是上下文切换抢占

5. What is a context switch? Preemption?

6. 解释调度循环的概念相对于多线程代码,它的优点和缺点是什么?

6. Explain the concept of a dispatch loop. What are its advantages and disadvantages with respect to multithreaded code?

7. 解释多处理器集群之间、处理器与核心之间的区别

7. Explain the distinction between a multiprocessor and a cluster; between a processor and a core.

8.多处理器中的内存 统一是什么意思? 替代方案是什么?

8. What does it mean for memory in a multiprocessor to be uniform? What is the alternative?

9. 解释多核和多处理器缓存的一致性问题。

9. Explain the coherence problem for multicore and multiprocessor caches.

 什么是向量机?向量技术在现代系统中出现在哪里?

. What is a vector machine? Where does vector technology appear in modern systems?

13.2 并发编程基础

13.2 Concurrent Programming Fundamentals

在并发程序中,我们将使用术语“线程”来指代程序员认为与其他线程并发运行的活动实体。在大多数系统中,给定程序的线程是在操作系统提供的一个或多个进程之上实现的。操作系统设计人员通常会区分重量级进程(具有自己的地址空间)和轻量级进程集合(可能共享一个地址空间)。在 20 世纪 80 年代末和 90 年代初,大多数 Unix 变体都添加了轻量级进程,以适应共享内存多处理器的普及。

Within a concurrent program, we will use the term thread to refer to the active entity that the programmer thinks of as running concurrently with other threads. In most systems, the threads of a given program are implemented on top of one or more processes provided by the operating system. OS designers often distinguish between a heavyweight process, which has its own address space, and a collection of lightweight processes, which may share an address space. Lightweight processes were added to most variants of Unix in the late 1980s and early 1990s, to accommodate the proliferation of shared-memory multiprocessors.

我们有时会使用“任务”一词来指代必须由某个线程执行的明确定义的工作单元。在一个常见的编程习语中,一组线程共享一个公共的“任务包”——要完成的工作列表。每个线程都会反复从任务包中取出一个任务,执行该任务,然后再返回执行另一个任务。有时,一项任务的工作需要向任务包中添加新任务。

We will sometimes use the word task to refer to a well-defined unit of work that must be performed by some thread. In one common programming idiom, a collection of threads shares a common “bag of tasks”—a list of work to be done. Each thread repeatedly removes a task from the bag, performs it, and goes back for another. Sometimes the work of a task entails adding new tasks to the bag.

不幸的是,不同系统和作者的术语并不一致。有几种语言将线程称为进程。Ada 将它们称为任务。有几种操作系统将轻量级进程称为线程。OSF Unix 和 Mac OS X 源自 Mach OS,它将轻量级进程共享的地址空间称为任务。一些系统试图通过创造新词来避免歧义,例如“actors”、“fibers”或“filaments”。我们将尝试一致地使用前两段的定义,并找出特定语言或系统的术语与此用法不同的情况。

Unfortunately, terminology is inconsistent across systems and authors. Several languages call their threads processes. Ada calls them tasks. Several operating systems call lightweight processes threads. The Mach OS, from which OSF Unix and Mac OS X are derived, calls the address space shared by lightweight processes a task. A few systems try to avoid ambiguity by coining new words, such as “actors,” “fibers,” or “filaments.” We will attempt to use the definitions of the preceding two paragraphs consistently, and to identify cases in which the terminology of particular languages or systems differs from this usage.

13.2.1 通信和同步

13.2.1 Communication and Synchronization

在任何并发编程模型中,要解决的两个最关键问题是通信同步。通信是指允许一个线程获取另一个线程生成的信息的任何机制。命令式程序的通信机制通常基于共享内存消息传递。在共享内存编程模型中,程序的部分或全部变量可供多个线程访问。对于要进行通信的两个线程,其中一个线程将值写入变量,而另一个线程则只是读取该值。在纯消息传递编程模型中,线程没有共同的状态:对于要进行通信的两个线程,其中一个线程必须执行显式发送操作才能将数据传输给另一个线程。(某些语言(例如 Ada、Go 和 Rust)同时提供消息共享内存。)

In any concurrent programming model, two of the most crucial issues to be addressed are communication and synchronization. Communication refers to any mechanism that allows one thread to obtain information produced by another. Communication mechanisms for imperative programs are generally based on either shared memory or message passing. In a shared-memory programming model, some or all of a program's variables are accessible to multiple threads. For a pair of threads to communicate, one of them writes a value to a variable and the other simply reads it. In a pure message-passing programming model, threads have no common state: for a pair of threads to communicate, one of them must perform an explicit send operation to transmit data to another. (Some languages—Ada, Go, and Rust, for example—provide both messages and shared memory.)

同步是指允许程序员控制不同线程中操作发生的相对顺序的任何机制。同步在消息传递模型中通常是隐式的:必须先发送消息,然后才能接收。如果线程尝试接收尚未发送的消息,它将等待发送者赶上。同步在共享内存模型中通常不是隐式的:除非我们做一些特殊的事情,否则“接收”线程可能会在“发送者”写入变量之前读取该变量的“旧”值。

Synchronization refers to any mechanism that allows the programmer to control the relative order in which operations occur in different threads. Synchronization is generally implicit in message-passing models: a message must be sent before it can be received. If a thread attempts to receive a message that has not yet been sent, it will wait for the sender to catch up. Synchronization is generally not implicit in shared-memory models: unless we do something special, a “receiving” thread could read the “old” value of a variable, before it has been written by the “sender.”

在共享内存和基于消息的程序中,同步都可以通过旋转(也称为忙等待)或阻塞来实现。在忙等待同步中,线程运行一个循环,在该循环中,它不断重新评估某些条件,直到该条件变为真(例如,直到消息队列变为非空或共享变量达到特定值)——可能是由于在其他核心上运行的其他线程中的操作而导致的。请注意,忙等待在单处理器上毫无意义:我们不能指望在独占使条件成立所需的资源(唯一的核心)时条件会变为真。(单处理器上的线程有时可能会忙于等待 I/O 完成,但那是另一种情况:I/O 设备与处理器并行运行。)

In both shared-memory and message-based programs, synchronization can be implemented either by spinning (also called busy-waiting) or by blocking. In busy-wait synchronization, a thread runs a loop in which it keeps reevaluating some condition until that condition becomes true (e.g., until a message queue becomes nonempty or a shared variable attains a particular value)—presumably as a result of action in some other thread, running on some other core. Note that busy-waiting makes no sense on a uniprocessor: we cannot expect a condition to become true while we are monopolizing a resource (the one and only core) required to make it true. (A thread on a uniprocessor may sometimes busy-wait for the completion of I/O, but that's a different situation: the I/O device runs in parallel with the processor.)

在阻塞同步(也称为基于调度程序的同步)中,等待线程自愿将其核心让给其他线程。在这样做之前,它会在与同步条件关联的某个数据结构中留下一条注释。在未来某个时间点使条件成立的线程将找到该注释并采取行动使阻塞线程再次运行。我们将在13.2.4 节中再次简要讨论同步,然后在13.3 节中更详细地讨论同步。

In blocking synchronization (also called scheduler-based synchronization), the waiting thread voluntarily relinquishes its core to some other thread. Before doing so, it leaves a note in some data structure associated with the synchronization condition. A thread that makes the condition true at some point in the future will find the note and take action to make the blocked thread run again. We will consider synchronization again briefly in Section 13.2.4, and then more thoroughly in Section 13.3.

设计与实现

Design & Implementation

13.2 硬件和软件通信

13.2 Hardware and software communication

如第 13.1.2 节所述,共享内存和消息传递之间的区别不仅适用于语言和库,也适用于计算机硬件。需要注意的是,语言或库提供的通信和同步模型不一定与底层硬件的模型一致。在共享内存硬件上实现消息传递很容易。再多花点功夫,也可以在消息传递硬件上实现共享内存。后者的系统有时被称为软件分布式共享内存(S-DSM)。

As described in Section 13.1.2, the distinction between shared memory and message passing applies not only to languages and libraries but also to computer hardware. It is important to note that the model of communication and synchronization provided by the language or library need not necessarily agree with that of the underlying hardware. It is easy to implement message passing on top of shared-memory hardware. With a little more effort, one can also implement shared memory on top of message-passing hardware. Systems in this latter camp are sometimes referred to as software distributed shared memory (S-DSM).

13.2.2 语言和库

13.2.2 Languages and Libraries

线程级并发性可以以显式并发语言、编译器支持的传统顺序语言扩展或语言本身之外的库包的形式提供给程序员。这三种选项都被广泛使用,尽管共享内存语言在“低端”(用于多核和小型多处理器机器)更常见,而消息传递库在“高端”(用于大规模并行超级计算机)更常见。图 13.4分类了广泛使用的系统示例。

Thread-level concurrency can be provided to the programmer in the form of explicitly concurrent languages, compiler-supported extensions to traditional sequential languages, or library packages outside the language proper. All three options are widely used, though shared-memory languages are more common at the “low end” (for multicore and small multiprocessor machines), and message-passing libraries are more common at the “high end” (for massively parallel supercomputers). Examples of systems in widespread use are categorized in Figure 13.4.

f13-04-9780124104099
图 13.4 并行编程系统的示例。表中每个区域还有大量实验性、教学性或利基性提案。

多年来,几乎所有的并行编程都采用传统的顺序语言(主要是 C 和 Fortran),并增加了用于同步或消息传递的库,这种方法至今仍占主导地位。在 Unix 世界中,共享内存并行性已基本融合到 POSIX pthreads标准中,该标准包括创建、销毁、调度和同步线程的机制。自 2011 年版以来,该标准已成为 C 和 C++ 的正式组成部分。Microsoft 的线程包和编译器为 Windows 计算机提供了类似的功能。对于高端科学计算,基于消息的并行性同样融合到 MPI(消息传递接口)标准中,几乎每个平台都有开源和商业实现。

For many years, almost all parallel programming employed traditional sequential languages (largely C and Fortran) augmented with libraries for synchronization or message passing, and this approach still dominates today. In the Unix world, shared memory parallelism has largely converged on the POSIX pthreads standard, which includes mechanisms to create, destroy, schedule, and synchronize threads. This standard became an official part of both C and C++ as of their 2011 versions. Similar functionality for Windows machines is provided by Microsoft's thread package and compilers. For high-end scientific computing, message-based parallelism has likewise converged on the MPI (Message Passing Interface) standard, with open-source and commercial implementations available for almost every platform.

虽然语言对并发的支持可以追溯到 Algol 68(而协程可以追溯到 Simula),并且这种支持在 20 世纪 80 年代末在 Ada 中得到了广泛应用,但直到 20 世纪 90 年代中期,人们才真正开始对这些功能产生广泛的兴趣,当时万维网的​​爆炸式增长开始推动并行服务器和并发客户端程序的发展。这一发展恰好与 Java 的推出相吻合,几年后微软又推出了 C#。尽管影响力还不如 C#,但许多其他语言,包括 Erlang、Go、Haskell、Rust 和 Scala,也明确支持并行。

While language support for concurrency goes back all the way to Algol 68 (and coroutines to Simula), and while such support was widely available in Ada by the late 1980s, widespread interest in these features didn't really arise until the mid-1990s, when the explosive growth of the World Wide Web began to drive the development of parallel servers and concurrent client programs. This development coincided nicely with the introduction of Java, and Microsoft followed with C# a few years later. Though not yet as influential, many other languages, including Erlang, Go, Haskell, Rust, and Scala, are explicitly parallel as well.

在科学编程领域,Fortran 的扩展历史悠久,旨在促进循环迭代的并行执行。到本世纪初,这项工作已基本集中在一组称为 OpenMP 的扩展上,不仅在 Fortran 中可用,而且在 C 和 C++ 中也可用。从语法上讲,OpenMP 包含一组编译指令(编译器指令),用于创建和同步线程,并在线程之间安排工作。在由多处理器网络组成的机器上,越来越常见的是看到在多处理器中使用 OpenMP 并在多处理器之间使用 MPI 的混合程序。

In the realm of scientific programming, there is a long history of extensions to Fortran designed to facilitate the parallel execution of loop iterations. By the turn of the century this work had largely converged on a set of extensions known as OpenMP, available not only in Fortran but also in C and C++. Syntactically, OpenMP comprises a set of pragmas (compiler directives) to create and synchronize threads, and to schedule work among them. On machines composed of a network of multiprocessors, it is increasingly common to see hybrid programs that use OpenMP within a multiprocessor and MPI across them.

在图 13.4的共享内存和消息传递列中,并行构造旨在用于单个多线程程序中。对于分布式系统中跨程序边界的通信,程序员传统上采用标准 Internet 协议的库实现,其方式让人联想到基于文件的 I/O(第 C-8.7 节)。然而,对于客户端-服务器交互,提供基于远程过程调用(RPC)的更高级接口可能很有吸引力,我们将在 C-13.5.4 节中进一步讨论这种替代方案。

In both the shared memory and message passing columns of Figure 13.4, the parallel constructs are intended for use within a single multithreaded program. For communication across program boundaries in distributed systems, programmers have traditionally employed library implementations of the standard Internet protocols, in a manner reminiscent of file-based I/O (Section C-8.7). For client-server interaction, however, it can be attractive to provide a higher-level interface based on remote procedure calls (RPC), an alternative we consider further in Section C-13.5.4.

与库包相比,显式并发编程语言具有编译器支持的优势。它可以使用除子例程调用之外的其他语法,并且可以将通信和线程管理与类型检查、作用域和异常等概念更紧密地集成在一起。同时,由于大多数程序历来都是顺序的,因此并发语言很难获得广泛接受,特别是考虑到并发功能的存在有时会使顺序情况更难理解。

In comparison to library packages, an explicitly concurrent programming language has the advantage of compiler support. It can make use of syntax other than subroutine calls, and can integrate communication and thread management more tightly with such concepts as type checking, scoping, and exceptions. At the same time, since most programs have historically been sequential, concurrent languages have been slow to gain widespread acceptance, particularly given that the presence of concurrent features can sometime make the sequential case more difficult to understand.

13.2.3 线程创建语法

13.2.3 Thread Creation Syntax

几乎每个并发系统都允许动态创建(和销毁)线程。不同语言或库的语法和语义细节差别很大,但大多数都符合六个主要选项之一:co-begin、并行循环、launch-at-elaboration、fork(带有可选的join)、隐式接收和早期回复。前两个选项使用特殊的控制流构造来界定线程。其他选项使用类似于(或等同于)子例程的语法。

Almost every concurrent system allows threads to be created (and destroyed) dynamically. Syntactic and semantic details vary considerably from one language or library to another, but most conform to one of six principal options: co-begin, parallel loops, launch-at-elaboration, fork (with optional join), implicit receipt, and early reply. The first two options delimit threads with special control-flow constructs. The others use syntax resembling (or identical to) subroutines.

至少有一种教学语言 (SR) 提供了所有六个选项。大多数其他语言则选择其中一种。大多数库使用fork/join,Java 和 C# 也是如此。Ada 同时使用 launch-at-elaboration 和fork。OpenMP 使用co-begin和 parallel 循环。RPC 系统通常基于隐式接收。

At least one pedagogical language (SR) provided all six options. Most others pick and choose. Most libraries use fork/join, as do Java and C#. Ada uses both launch-at-elaboration and fork. OpenMP uses co-begin and parallel loops. RPC systems are typically based on implicit receipt.

共同开始

Co-begin

例 13.6

Example 13.6

co-gin的一般形式

General form of co-begin

复合语句(有时以begin…end分隔)的通常语义要求顺序执行组成语句。co-begin构造则要求并发执行:

The usual semantics of a compound statement (sometimes delimited with begin…end) call for sequential execution of the constituent statements. A co-begin construct calls instead for concurrent execution:

co-begin –– 所有n 个语句同时运行

co-begin        –– all n statements run concurrently

           语句1

           stmt_1

           语句2

           stmt_2

stmt_n

stmt_n

结尾

end

每个语句本身可以是顺序或并行的复合语句,或者(通常)是子程序调用。■

Each statement can itself be a sequential or parallel compound, or (commonly) a subroutine call. ■

例 13.7

Example 13.7

OpenMP 中的共同开始

Co-begin in OpenMP

Co-begin是 Algol-68 中创建线程的主要方法。它也出现在各种其他系统中,包括 OpenMP:

Co-begin was the principal means of creating threads in Algol-68. It appears in a variety of other systems as well, including OpenMP:

#pragma omp 部分

#pragma omp sections

{

{

# pragma omp 部分

#  pragma omp section

   { printf(“线程 1 在此\n”); }

   { printf(“thread 1 here\n”); }

# pragma omp 部分

#  pragma omp section

   { printf(“线程 2 在此\n”); }

   { printf(“thread 2 here\n”); }

}

}

在 C 语言中,OpenMP 指令全部以#pragma om p 开头。(#符号必须出现在第一列。)大多数指令(如此处所示的指令)必须紧接在循环结构或用花括号分隔的复合语句之前。■

In C, OpenMP directives all begin with #pragma omp. (The # sign must appear in column one.) Most directives, like those shown here, must appear immediately before a loop construct or a compound statement delimited with curly braces. ■

并行循环

Parallel Loops

例 13.8

Example 13.8

OpenMP 中的并行循环

A parallel loop in OpenMP

许多并发系统(包括 OpenMP、Fortran 的几种方言以及 .NET 的任务并行库)都提供了一个循环,其迭代将并发执行。在 C 的 OpenMP 中,我们可以说

Many concurrent systems, including OpenMP, several dialects of Fortran, and the Task Parallel Library for .NET, provide a loop whose iterations are to be executed concurrently. In OpenMP for C, we might say

#pragma omp 并行

#pragma omp parallel for

对于 (int i = 0; i < 3; i++) {

for (int i = 0; i < 3; i++) {

  printf(“线程 %d 在此\n”, i);

  printf(“thread %d here\n”, i);

}

}

例 13.9

Example 13.9

C# 中的并行循环

A parallel loop in C#

在带有任务并行库的 C# 中,等效代码如下所示:

In C# with the Task Parallel Library, the equivalent code looks like this:

并行.For(0, 3, i => {

Parallel.For(0, 3, i => {

  Console.WriteLine(“线程“ + i + “这里”);

  Console.WriteLine(“Thread “ + i + “here”);

});

});

Parallel.For的第三个参数是委托,在本例中是 lambda 表达式。类似的Foreach方法需要两个参数——一个迭代器和一个委托。■

The third argument to Parallel.For is a delegate, in this case a lambda expression. A similar Foreach method expects two arguments—an iterator and a delegate. ■

在许多系统中,程序员有责任确保循环迭代的并发执行是安全的,即正确性永远不会取决于竞争条件的结果。例如,对全局变量的访问通常必须同步,以确保迭代不会相互冲突。在一些语言(例如 Occam)中,语言规则禁止冲突访问。编译器会检查以确保一个线程写入的变量不会被任何并发活动线程读取或写入。类似但稍微灵活一些的是, Fortran 2008 的do parallel循环构成了程序员方面的断言,即循环的迭代是相互独立的,因此可以安全地按任何顺序或并行执行。循环内容的几条规则(其中一些但不是全部由编译器强制执行)降低了程序员错误地做出此断言的可能性。

In many systems it is the programmer's responsibility to make sure that concurrent execution of the loop iterations is safe, in the sense that correctness will never depend on the outcome of race conditions. Access to global variables, for example, must generally be synchronized, to make sure that iterations do not conflict with one another. In a few languages (e.g., Occam), language rules prohibit conflicting accesses. The compiler checks to make sure that a variable written by one thread is neither read nor written by any concurrently active thread. In a similar but slightly more flexible vein, the do concurrent loop of Fortran 2008 constitutes an assertion on the programmer's part that iterations of the loop are mutually independent, and hence can safely be executed in any order, or in parallel. Several rules on the content of the loop—some but not all of them enforceable by the compiler—reduce the likelihood that programmers will make this assertion incorrectly.

例 13.10

Example 13.10

Fortran 95 中的Forall

Forall in Fortran 95

从历史上看,Fortran 的几种并行方言提供了其他形式的并行循环,语义各不相同。高性能 Fortran (HPF) 的forall循环随后被纳入 Fortran 95。与do parallel一样,它表示迭代可以并行进行。但是,为了解决竞争条件,它对循环的组成语句施加了自动的内部同步,每个语句都必须是赋值或嵌套的forall循环。具体来说,在所有迭代中,给定赋值语句中变量的所有读取都必须发生在任何迭代中对左侧的任何写入之前。而左侧的写入又必须发生在下一个赋值语句中的任何读取之前。在以下示例中,循环中的第一个赋值将读取B的n - 1 个元素和C的n - 1 个元素,然后更新A的n - 1 个元素。随后,第二个赋值语句将读取A的所有n 个元素,然后更新其中的n - 1 个:

Historically, several parallel dialects of Fortran provided other forms of parallel loop, with varying semantics. The forall loop of High Performance Fortran (HPF) was subsequently incorporated into Fortran 95. Like do concurrent, it indicates that iterations can proceed in parallel. To resolve race conditions, however, it imposes automatic, internal synchronization on the constituent statements of the loop, each of which must be an assignment or a nested forall loop. Specifically, all reads of variables in a given assignment statement, in all iterations, must occur before any write to the left-hand side, in any iteration. The writes of the left-hand side in turn must occur before any reads in the next assignment statement. In the following example, the first assignment in the loop will read n − 1 elements of B and n − 1 elements of C, and then update n − 1 elements of A. Subsequently, the second assignment statement will read all n elements of A and then update n − 1 of them:

对于所有 (i=1:n-1)

forall (i=1:n-1)

 A(i) = B(i) + C(i)

 A(i) = B(i) + C(i)

 A(i+1) = A(i) + A(i+1)

 A(i+1) = A(i) + A(i+1)

结束所有

end forall

特别要注意的是,第一个赋值语句中对A(i)的所有更新都发生在第二个赋值语句中的任何读取之前。此外,在第二个赋值语句中,对A(i+1)的更新在“后续”迭代中对A(i)的读取中是看不到的:迭代并行发生,并且每次迭代都会在更新其左侧之前读取其右侧的变量。■

Note in particular that all of the updates to A(i) in the first assignment statement occur before any of the reads in the second assignment statement. Moreover in the second assignment statement the update to A(i+1) is not seen by the read of A(i) in the “subsequent” iteration: the iterations occur in parallel and each reads the variables on its right-hand side before updating its left-hand side. ■

对于遍历数组元素的循环,forall语义非常适合在向量机上执行。对于更传统的多处理器,HPF 提供了一组广泛的数据分布对齐指令,允许程序员将元素分散到与大量处理器相关的内存中。在forall循环中,给定赋值语句中的计算通常由“拥有”赋值左侧元素的处理器执行。在许多情况下,HPF 或 Fortran 95 编译器可以证明forall循环的某些(部分)组成语句之间没有依赖关系,并且可以允许它们继续进行而无需实际实现同步。

For loops that iterate over the elements of an array, the forall semantics are ideally suited for execution on a vector machine. For more conventional multiprocessors, HPF provides an extensive set of data distribution and alignment directives that allow the programmer to scatter elements across the memory associated with a large number of processors. Within a forall loop, the computation in a given assignment statement is usually performed by the processor that “owns” the element on the assignment's left-hand side. In many cases an HPF or Fortran 95 compiler can prove that there are no dependences among certain (portions of) constituent statements of a forall loop, and can allow them to proceed without actually implementing synchronization.

例 13.11

Example 13.11

OpenMP 的减少

Reduction in OpenMP

OpenMP 不强制forall的逐语句同步,但它确实为调度和数据管理提供了重要支持。并行指令上的可选“子句”可以指定有多少个线程创建,以及在哪个线程中执行循环的哪些迭代。他们还可以指定哪些程序变量应该由所有线程共享,哪些程序变量应该拆分为每个线程的单独副本。甚至可以指定在循环结束时应使用交换运算符在所有线程中减少私有变量。例如,要对非常大的向量的元素求和,可以写

OpenMP does not enforce the statement-by-statement synchronization of forall, but it does provide significant support for scheduling and data management. Optional “clauses” on parallel directives can specify how many threads to create, and which iterations of the loop to perform in which thread. They can also specify which program variables should be shared by all threads, and which should be split into a separate copy for each thread. It is even possible to specify that a private variable should be reduced across all threads at the end of the loop, using a commutative operator. To sum the elements of a very large vector, for example, one might write

双A[N];

double A[N];

双精度和 = 0;

double sum = 0;

#pragma omp parallel 用于调度(静态)\

#pragma omp parallel for schedule(static) \

  默认(共享)减少(+:总和)

  default(shared) reduction(+:sum)

对于 (int i = 0; i < N; i++) {

for (int i = 0; i < N; i++) {

  总和 += A[i] ;

  sum += A[i] ;

}

}

printf(“并行总和:%f\n”,sum);

printf(“parallel sum: %f\n”, sum);

此处的schedule(static)子句表示编译器应将迭代均匀地划分到线程之间,以连续的组为单位。因此,如果有t 个线程,则第一个线程应获得前N/t次迭代,第二个线程应获得接下来的N/t次迭代,依此类推。default (shared)子句表示所有变量(除i外)应由所有线程共享,除非另有规定。reduction (+:sum)子句使sum成为一个例外:每个线程都应有自己的副本(从循环前有效的值初始化),并且副本应在最后合并(使用+ )。如果t很大,编译器可能会使用深度为 log( t )的树对值求和。■

Here the schedule(static) clause indicates that the compiler should divide the iterations evenly among threads, in contiguous groups. So if there are t threads, the first thread should get the first N/t iterations, the second should get the next N/t iterations, and so on. The default(shared) clause indicates that all variables (other than i) should be shared by all threads, unless otherwise specified. The reduction(+:sum) clause makes sum an exception: every thread should have its own copy (initialized from the value in effect before the loop), and the copies should be combined (with +) at the end. If t is large, the compiler will probably sum the values using a tree of depth log(t). ■

细化阶段启动

Launch-at-Elaboration

例 13.12

Example 13.12

Ada 中的精细任务

Elaborated tasks in Ada

在几种语言中,包括 Ada,线程代码可能使用类似于没有参数的子程序的语法来声明。声明完成后,会创建一个线程来执行代码。在 Ada(将其线程称为任务)中,我们可以这样写

In several languages, Ada among them, the code for a thread may be declared with syntax resembling that of a subroutine with no parameters. When the declaration is elaborated, a thread is created to execute the code. In Ada (which calls its threads tasks) we may write

程序 P 是

procedure P is

任务T是

task T is

结束T;

end T;

开始——P

begin -— P

结束P;

end P;

任务T有自己的begin…end块,一旦控制进入过程P ,它就会开始执行。如果P是递归的,则可能同时存在许多T实例,所有这些实例都彼此并发执行,并与正在执行的任务(当前实例)P并发执行。主程序的行为类似于初始默认任务。

Task T has its own begin…end block, which it begins to execute as soon as control enters procedure P. If P is recursive, there may be many instances of T at the same time, all of which execute concurrently with each other and with whatever task is executing (the current instance of) P. The main program behaves like an initial default task.

当控制到达过程P的末尾时,它将等待T的相应实例(在P的此实例开始时创建的实例)完成后再返回。此规则确保P的局部变量(在通常的静态作用域规则下对T可见)在T完成之前永远不会被释放。■

When control reaches the end of procedure P, it will wait for the appropriate instance of T (the one that was created at the beginning of this instance of P) to complete before returning. This rule ensures that the local variables of P (which are visible to T under the usual static scope rules) are never deallocated before T is done with them. ■

分叉/合并

Fork/Join

例 13.13

Example 13.13

共同开始与分叉/加入

Co-begin vs fork/join

共同开始、并行循环和启动时展开都会导致并发控制流模式,其中线程执行是正确嵌套的(见图13.5a)。fork操作更通用:它使线程的创建成为显式的可执行操作。如果提供伴随的join操作,允许线程等待先前分叉的线程完成。由于forkjoin不依赖于嵌套构造,因此它们可以导致任意的并发控制流模式(图 13.5b)。■

Co-begin, parallel loops, and launch-at-elaboration all lead to a concurrent control-flow pattern in which thread executions are properly nested (see Figure 13.5a). The fork operation is more general: it makes the creation of threads an explicit, executable operation. The companion join operation, when provided, allows a thread to wait for the completion of a previously forked thread. Because fork and join are not tied to nested constructs, they can lead to arbitrary patterns of concurrent control flow (Figure 13.5b). ■

f13-05-9780124104099
图 13.5 并发线程的生命周期。使用co-begin、并行循环或 launch-at-elaboration (a),线程始终可以正确嵌套。使用fork/join (b),可以实现更通用的模式。

例 13.14

Example 13.14

Ada 中的任务类型

Task types in Ada

除了提供启动时详细说明的任务之外,Ada 还允许程序员定义任务类型:

In addition to providing launch-at-elaboration tasks, Ada allows the programmer to define task types:

任务类型 T 是

task type T is

开始

begin

结束T;

end T;

然后,程序员可以声明访问类型 T(指向T的指针)的变量,并且可以通过动态分配来创建新任务:

The programmer may then declare variables of type access T (pointer to T), and may create new tasks via dynamic allocation:

pt:访问T:=新T;

pt : access T := new T;

操作是分叉:它创建一个新线程并开始执行。Ada 中没有显式的联接操作,尽管父任务和子任务始终可以根据需要显式地相互同步(例如,在子任务完成执行之前立即同步)。与启动时阐述一样,控制将在任何声明了任务类型的范围结束时自动等待,以便所有使用该范围的线程终止。■

The new operation is a fork : it creates a new thread and starts it executing. There is no explicit join operation in Ada, though parent and child tasks can always synchronize with one another explicitly if desired (e.g., immediately before the child completes its execution). As with launch-at-elaboration, control will wait automatically at the end of any scope in which task types are declared for all threads using the scope to terminate. ■

例 13.15

Example 13.15

Java 2 中的线程创建

Thread creation in Java 2

Ada 任务完成其工作所需的任何信息都必须通过共享变量或任务开始执行后发送的显式消息进行通信。相比之下,大多数系统允许在启动时将参数传递给线程。在 Java 中,可以通过构造从预定义类Thread派生的某个类的对象来获取线程:

Any information an Ada task needs in order to do its job must be communicated through shared variables or through explicit messages sent after the task has started execution. Most systems, by contrast, allow parameters to be passed to a thread at start-up time. In Java one obtains a thread by constructing an object of some class derived from a predefined class called Thread:

ImageRenderer 类扩展了 Thread {

class ImageRenderer extends Thread {

 

 

 图像渲染器(参数){

 ImageRenderer(args) {

  // 构造函数

  // constructor

 }

 }

设计与实现

Design & Implementation

13.3 任务并行和数据并行计算

13.3 Task-parallel and data-parallel computing

程序员在编写并行程序时必须做出的最基本决定之一就是如何在线程之间分配工作。一种常见策略在小型机器上效果很好,即对程序的每个主要任务或功能使用单独的线程,并以流水线方式或以其他方式重叠执行它们。例如,在文字处理器中,一个线程可能用于将段落分成行,另一个线程用于分页和图形放置,另一个线程用于拼写和语法检查,另一个线程用于在屏幕上渲染图像。这种策略通常称为任务并行。它的主要缺点是它不能自然扩展到非常大量的处理器。为此,通常需要数据并行,其中或多或少相同的操作会同时应用于一些非常大的数据集的元素。例如,图像处理程序可以将屏幕分成n 个小块,并使用单独的线程来处理每个块。游戏可以为每个移动角色或对象使用单独的线程。

One of the most basic decisions a programmer has to make when writing a parallel program is how to divide work among threads. One common strategy, which works well on small machines, is to use a separate thread for each of the program's major tasks or functions, and to pipeline or otherwise overlap their execution. In a word processor, for example, one thread might be devoted to breaking paragraphs into lines, another to pagination and figure placement, another to spelling and grammar checking, and another to rendering the image on the screen. This strategy is often known as task parallelism. Its principal disadvantage is that it doesn't naturally scale to very large numbers of processors. For that, one generally needs data parallelism, in which more or less the same operations are applied concurrently to the elements of some very large data set. An image manipulation program, for example, may divide the screen into n small tiles, and use a separate thread to process each tile. A game may use a separate thread for every moving character or object.

功能专为数据并行性而设计的编程系统有时被称为数据并行语言或库。任务并行程序通常基于co-begin、launch-at-elaboration 或 fork/join:不同线程中的代码可能不同。数据并行程序通常基于并行循环:每个线程使用不同的数据执行相同的代码。OpenCL 和 CUDA 毫不奇怪地属于数据并行阵营:可编程 GPU 针对数据并行程序进行了优化。

A programming system whose features are designed for data parallelism is sometimes referred to as a data-parallel language or library. Task parallel programs are commonly based on co-begin, launch-at-elaboration, or fork/join: the code in different threads can be different. Data parallel programs are commonly based on parallel loops: each thread executes the same code, using different data. OpenCL and CUDA, unsurprisingly, are in the data-parallel camp: programmable GPUs are optimized for data parallel programs.

 公共无效运行(){

 public void run() {

  // 线程要运行的代码

  // code to be run by the thread

 }

 }

}

}

ImageRenderer rend = new ImageRenderer(构造函数参数);

ImageRenderer rend = new ImageRenderer(constructor-args);

从表面上看, new的使用类似于 Ada 中动态任务的创建。然而,在 Java 中,新线程在首次创建时并不开始执行。要启动它,父线程(或其他线程)必须调用名为start 的方法,该方法在Thread中定义:

Superficially, the use of new resembles the creation of dynamic tasks in Ada. In Java, however, the new thread does not begin execution when first created. To start it, the parent (or some other thread) must call the method named start, which is defined in Thread:

rend. 开始();

rend.start();

Start使线程可运行,安排它执行其run方法,并返回调用者。程序员必须在从Thread派生的每个类中定义适当的run方法。run方法只能由start调用;程序员不应直接调用它,也不应重新定义start。还有一个join方法:

Start makes the thread runnable, arranges for it to execute its run method, and returns to the caller. The programmer must define an appropriate run method in every class derived from Thread. The run method is meant to be called only by start; programmers should not call it directly, nor should they redefine start. There is also a join method:

rend.join(); // 等待完成

rend.join();  // wait for completion

例 13.16

Example 13.16

在 C# 中创建线程

Thread creation in C#

Java 线程的构造函数通常将其参数保存在稍后由run访问的字段中。实际上,从Thread派生的类充当对象闭包,如第 3.6.3 节所述。多种语言(其中包括 Modula-3 和 C#)更明确地使用闭包。C# 允许从任意ThreadStart委托创建一个线程,而不是要求每个线程都从通用Thread类派生:

The constructor for a Java thread typically saves its arguments in fields that are later accessed by run. In effect, the class derived from Thread functions as an object closure, as described in Section 3.6.3. Several languages, Modula-3 and C# among them, use closures more explicitly. Rather than require every thread to be derived from a common Thread class, C# allows one to be created from an arbitrary ThreadStart delegate:

图像渲染器类 {

class ImageRenderer {

 

 

 公共 ImageRenderer(参数){

 public ImageRenderer(args) {

  // 构造函数

  // constructor

 }

 }

 public void Foo() { // Foo 与 ThreadStart 兼容;

 public void Foo() { // Foo is compatible with ThreadStart;

      // 它的名字并不重要

      // its name is not significant

  // 线程要运行的代码

  // code to be run by the thread

 }

 }

}

}

ImageRenderer rendObj = new ImageRenderer(构造函数参数);

ImageRenderer rendObj = new ImageRenderer(constructor_args);

线程rend = 新线程(new ThreadStart(rendObj.Foo));

Thread rend = new Thread(new ThreadStart(rendObj.Foo));

如果可以从本地上下文中收集线程参数,那么甚至可以写成

If thread arguments can be gleaned from the local context, this can even be written as

线程 rend = new Thread(delegate() {

Thread rend = new Thread(delegate() {

 // 线程要运行的代码

 // code to be run by the thread

});

});

(请记住,C# 对匿名委托具有无限的范围。)无论哪种方式,新线程都会启动并等待,就像在 Java 中一样:

(Remember, C# has unlimited extent for anonymous delegates.) Either way, the new thread is started and awaited just as it is in Java:

rend.开始();

rend.Start();

连接();

rend.Join();

例 13.17

Example 13.17

Java 5 中的线程池

Thread pools in Java 5

从 Java 5(及其java.util.concurrent库)开始,不鼓励程序员显式创建线程。相反,要完成的任务由支持 Runnable 接口的对象表示这些对象被传递给Executor对象。Executor反过来将它们分发给托管线程池:

As of Java 5 (with its java.util.concurrent library), programmers are discouraged from creating threads explicitly. Rather, tasks to be accomplished are represented by objects that support the Runnable interface, and these are passed to an Executor object. The Executor in turn farms them out to a managed pool of threads:

类 ImageRenderer 实现 Runnable {

class ImageRenderer implements Runnable {

 

 

 // 构造函数和 run() 方法与之前相同

 // constructor and run() method same as before

}

}

执行器池 = Executors.newFixedThreadPool(4);

Executor pool = Executors.newFixedThreadPool(4);

池.执行(新的ImageRenderer(构造函数参数));

pool.execute(new ImageRenderer(constructor_args ));

这里newFixedThreadPool的参数(大量标准Executor 工厂之一)指示应管理四个线程。pool.execute 调用中指定的每个任务都将由其中一个线程运行。通过分离任务和线程的概念,Java 允许程序员(或运行时代码)选择一个Executor类,其真正的并发级别和调度规则适合底层操作系统和硬件。(在这个例子中,我们使用了一个特别简单的池,只有四个线程。)C# 具有类似的线程池功能。与 C# 线程一样,它们基于委托。■

Here the argument to newFixedThreadPool (one of a large number of standard Executor factories) indicates that pool should manage four threads. Each task specified in a call to pool.execute will be run by one of these threads. By separating the concepts of task and thread, Java allows the programmer (or run-time code) to choose an Executor class whose level of true concurrency and scheduling discipline are appropriate to the underlying OS and hardware. (In this example we have used a particularly simple pool, with exactly four threads.) C# has similar thread pool facilities. Like C# threads, they are based on delegates. ■

例 13.18

Example 13.18

在 Cilk 中生成同步

Spawn and sync in Cilk

Cilk 编程语言中出现了一种非常优雅的forkjoin实现,该语言由麻省理工学院的研究人员开发,后来发展成为一家商业企业,被英特尔收购。要在 Cilk 中 fork 逻辑并发任务,只需在普通函数调用前添加关键字spawn

A particularly elegant realization of fork and join appears in the Cilk programming language, developed by researchers at MIT, and subsequently developed into a commercial venture acquired by Intel. To fork a logically concurrent task in Cilk, one simply prepends the keyword spawn to an ordinary function call:

生成 foo( args );

spawn foo(args);

稍后,对内置操作sync的调用将与调用任务先前生成的所有任务合并。Cilk 的主要创新是任务调度机制。语言实现包括一种高效的线程池机制,它以接近最少的上下文切换次数和跨线程自动负载平衡来深度探索任务创建图。Java 7为Executor服务添加了类似但更受限制的机制,即ForkJoinPool。■

At some later time, invocation of the built-in operation sync will join with all tasks previously spawned by the calling task. The principal innovation of Cilk is the mechanism for scheduling tasks. The language implementation includes a highly efficient thread pool mechanism that explores the task-creation graph depth first with a near-minimal number of context switches and automatic load balancing across threads. Java 7 added a similar but more restricted mechanism in the form of a ForkJoinPool for the Executor service. ■

隐式接收

Implicit Receipt

到目前为止,我们在所有示例中都假设新创建的线程将在创建者的地址空间中运行。在 RPC 系统中,通常希望自动创建新线程以响应来自其他地址空间的传入请求。服务器可以将通信通道绑定到本地线程体或子例程,而不是让现有线程执行接收操作。当请求传入时,将出现一个新线程来处理它。

We have assumed in all our examples so far that newly created threads will run in the address space of the creator. In RPC systems it is often desirable to create a new thread automatically in response to an incoming request from some other address space. Rather than have an existing thread execute a receive operation, a server can bind a communication channel to a local thread body or subroutine. When a request comes in, a new thread springs into existence to handle it.

实际上,绑定操作授予远程客户端在服务器地址空间内执行分叉的能力,尽管该过程通常不是完全自动化的。我们将在 C-13.5.4 节中更详细地讨论 RPC。

In effect, the bind operation grants remote clients the ability to perform a fork within the server's address space, though the process is often less than fully automatic. We will consider RPC in more detail in Section C-13.5.4.

提早回覆

Early Reply

例 13.19

Example 13.19

使用 fork/join 建模子程序

Modeling subroutines with fork/join

我们通常将顺序子程序视为单个线程,该线程保存其当前上下文(其程序计数器和寄存器),执行子程序,然后返回到之前执行的操作。但是,如果我们有两个线程,效果是一样的——一个执行调用者,另一个执行被调用者。在这种情况下,调用本质上是一对fork/join。调用者等待被调用者终止,然后继续执行。■

We normally think of sequential subroutines in terms of a single thread, which saves its current context (its program counter and registers), executes the subroutine, and returns to what it was doing before. The effect is the same, however, if we have two threads—one that executes the caller and another that executes the callee. In this case, the call is essentially a fork/join pair. The caller waits for the callee to terminate before continuing execution. ■

然而,没有规定被调用者必须终止才能释放调用者;它真正要做的就是完成其工作的一部分,结果参数依赖。从Simula 中用于启动协程的分离操作(示例 9.47)中汲取灵感,一些语言(其中包括SR 和 Hermes [ SBG + 91 ])允许被调用方执行回复操作,该操作会在不终止的情况下将结果返回给调用方。在早期回复后,两个线程将同时继续。

Nothing dictates, however, that the callee has to terminate in order to release the caller; all it really has to do is complete the portion of its work on which result parameters depend. Drawing inspiration from the detach operation used to launch coroutines in Simula (Example 9.47), a few languages (SR and Hermes [SBG+91] among them) allow a callee to execute a reply operation that returns results to the caller without terminating. After an early reply, the two threads continue concurrently.

设计与实现

Design & Implementation

13.4 违反直觉的实现

13.4 Counterintuitive implementation

在 13 章的过程中,我们看到了许多语言特性的实现可能与程序员的直觉相悖的情况。早期回复——通常将线程创建延迟到实际回复发生时——只是最新的例子。其他例子包括表达式求值顺序(第 6.1.4 节)、子程序内联(第 9.2.4 节)、尾递归(第 6.6.1 节)、激活记录的非堆栈分配(对于无限范围——第 3.6.2 节)、记录字段的无序甚至不连续布局(第 8.1.2 节)、中央引用表中的变量查找(第 C-3.4.2 节)、变量引用模型下的不可变对象(第 6.1.2 节)以及在具有不同类型参数的实例之间共享代码的泛型实现(第 7.3.1 节)。编译器可能会生成与其输入的形式和组织截然不同的代码,尤其是在更高级别的代码改进下。除非受到语言定义的其他限制,否则实现可以自由选择任何可证明与输入等同的翻译。

Over the course of 13 chapters we have seen numerous cases in which the implementation of a language feature may run counter to the programmer's intuition. Early reply—in which thread creation is usually delayed until the reply actually occurs—is but the most recent example. Others have included expression evaluation order (Section 6.1.4), subroutine in-lining (Section 9.2.4), tail recursion (Section 6.6.1), nonstack allocation of activation records (for unlimited extent—Section 3.6.2), out-of-order or even noncontiguous layout of record fields (Section 8.1.2), variable lookup in a central reference table (Section C-3.4.2), immutable objects under a reference model of variables (Section 6.1.2), and implementations of generics (Section 7.3.1) that share code among instances with different type parameters. A compiler may, particularly at higher levels of code improvement, produce code that differs dramatically from the form and organization of its input. Unless otherwise constrained by the language definition, an implementation is free to choose any translation that is provably equivalent to the input.

从语义上讲,被调用者在回复之前的部分与 Java 或 C# 线程的构造函数的作用非常相似;回复之后的部分则充当run方法的作用。通常的实现也类似,并且可能与程序员的直觉相反:由于早期回复是可选的,并且可以出现在任何子例程中,因此我们可以使用调用者的线程来执行被调用者的初始部分,并且仅当(并且如果)被调用者回复而不是返回时才创建新线程。

Semantically, the portion of the callee prior to the reply plays much the same role as the constructor of a Java or C# thread; the portion after the reply plays the role of the run method. The usual implementation is also similar, and may run counter to the programmer's intuition: since early reply is optional, and can appear in any subroutine, we can use the caller's thread to execute the initial portion of the callee, and create a new thread only when—and if—the callee replies instead of returning.

13.2.4 线程的实现

13.2.4 Implementation of Threads

例 13.20

Example 13.20

进程上的多路复用线程

Multiplexing threads on Processes

正如我们在第 13.2 节开头提到的,并发程序的线程通常在操作系统提供的一个或多个进程之上实现。在一个极端,我们可以为每个线程使用一个单独的操作系统进程;在另一个极端,我们可以将程序的所有线程多路复用到一个进程之上。在超级计算机上,每个并发活动都有一个单独的核心,或者在线程是相对重量级抽象的语言中(长寿命,由几十个而不是几千个创建),每个线程一个进程的极端情况通常是可以接受的。在单处理器上的简单语言中,所有线程在一个进程上的极端情况可能是可以接受的。许多语言实现采用中间方法,可能非常大量的线程运行在一些较少但并非平凡的进程之上(参见图 13.6)。■

As we noted near the beginning of Section 13.2, the threads of a concurrent program are usually implemented on top of one or more processes provided by the operating system. At one extreme, we could use a separate OS process for every thread; at the other extreme we could multiplex all of a program's threads on top of a single process. On a supercomputer with a separate core for every concurrent activity, or in a language in which threads are relatively heavyweight abstractions (long-lived, and created by the dozens rather than the thousands), the one-process-per-thread extreme is often acceptable. In a simple language on a uniprocessor, the all-threads-on-one-process extreme maybe acceptable. Many language implementations adopt an intermediate approach, with a potentially very large number of threads running on top of some smaller but nontrivial number of processes (see Figure 13.6). ■

f13-06-9780124104099
图 13.6 线程的两级实现。在库或语言运行时包中实现的线程调度程序,在一个或多个内核级进程之上复用线程,就像进程调度程序一样;在操作系统内核中实现的线程调度程序,在一个或多个物理核心之上复用进程。

将每个线程放在单独的进程中的问题在于,进程(即使是“轻量级”进程)在许多操作系统中都过于昂贵。由于它们是在内核中实现的,因此对它们执行任何操作都需要系统调用。由于它们是通用的,因此它们提供了大多数语言不需要但无论如何都必须付费的功能。(示例包括单独的地址空间、优先级、记帐信息以及信号和 I/O 接口,所有这些都超出了本书的范围。)在另一个极端,将所有线程放在单个进程之上有两个问题:首先,它妨碍了在多核或多处理器计算机上并行执行;其次,如果当前正在运行的线程发出阻塞的系统调用(例如,等待 I/O),则程序的其他线程都无法运行,因为单个进程被操作系统暂停。

The problem with putting every thread on a separate process is that processes (even “lightweight” ones) are simply too expensive in many operating systems. Because they are implemented in the kernel, performing any operation on them requires a system call. Because they are general purpose, they provide features that most languages do not need, but have to pay for anyway. (Examples include separate address spaces, priorities, accounting information, and signal and I/O interfaces, all of which are beyond the scope of this book.) At the other extreme, there are two problems with putting all threads on top of a single process: first, it precludes parallel execution on a multicore or multiprocessor machine; second, if the currently running thread makes a system call that blocks (e.g., waiting for I/O), then none of the program's other threads can run, because the single process is suspended by the OS.

在常见的两级并发组织(内核级进程之上的用户级线程)中,系统的两个级别上都出现了类似的代码:语言运行时系统在一个或多个进程之上实现线程,其方式与操作系统在一个或多个物理核心之上实现进程的方式非常相似。在本节的其余部分,我们将使用进程之上的线程这​​一术语。

In the common two-level organization of concurrency (user-level threads on top of kernel-level processes), similar code appears at both levels of the system: the language run-time system implements threads on top of one or more processes in much the same way that the operating system implements processes on top of one or more physical cores. We will use the terminology of threads on top of processes in the remainder of this section.

典型的实现从协程开始(第 9.5 节)。回想一下,协程是一种顺序控制流机制:程序员可以通过调用transfer操作来暂停当前协程并恢复特定替代方案。transfer参数通常是指向协程上下文块的指针。

The typical implementation starts with coroutines (Section 9.5). Recall that coroutines are a sequential control-flow mechanism: the programmer can suspend the current coroutine and resume a specific alternative by calling the transfer operation. The argument to transfer is typically a pointer to the context block of the coroutine.

要将协程转换为线程,我们需要执行三个步骤。首先,我们通过实现一个调度程序来隐藏 transfer 的参数,该调度程序会在当前线程让出核心时选择接下来要运行的线程。其次,我们实现一个抢占机制,该机制会定期自动暂停当前线程,让其他线程有机会运行。第三,我们允许多个 OS 进程(可能位于不同的核心上)共享描述线程集合的数据结构,以便线程可以在任何进程上运行。

To turn coroutines into threads, we proceed in a series of three steps. First, we hide the argument to transfer by implementing a scheduler that chooses which thread to run next when the current thread yields the core. Second, we implement a preemption mechanism that suspends the current thread automatically on a regular basis, giving other threads a chance to run. Third, we allow the data structures that describe our collection of threads to be shared by more than one OS process, possibly on separate cores, so that threads can run on any of the processes.

单处理器调度

Uniprocessor Scheduling

例 13.21

Example 13.21

单处理器上的协作多线程

Cooperative multithreading on a uniprocessor

图 13.7说明了简单调度程序使用的数据结构。在任何特定时间,线程要么被阻塞(即,为了同步),要么处于可运行状态。可运行线程可能实际上正在某个进程上运行,也可能正在等待机会。可运行但当前未运行的线程的上下文块驻留在称为就绪列表的队列中。被基于调度程序的同步阻塞的线程的上下文块驻留在与它们正在等待的条件相关联的数据结构(通常是队列)中。要将核心让给另一个线程,正在运行的线程会调用调度程序:

Figure 13.7 illustrates the data structures employed by a simple scheduler. At any particular time, a thread is either blocked (i.e., for synchronization) or runnable. A runnable thread may actually be running on some process or it maybe awaiting its chance to do so. Context blocks for threads that are runnable but not currently running reside on a queue called the ready list. Context blocks for threads that are blocked for scheduler-based synchronization reside in data structures (usually queues) associated with the conditions for which they are waiting. To yield the core to another thread, a running thread calls the scheduler:

f13-07-9780124104099
图 13.7 简单调度程序的数据结构。指定的当前线程正在运行。就绪列表中的线程可运行。其他线程被阻止,等待各种条件变为真。如果线程在多个操作系统级进程之上运行,则每个此类进程都将拥有自己的current_thread变量。如果线程调用操作系统,其进程可能会在内核中阻塞。

程序重新安排()

procedure reschedule()

 t:线程:=出队(ready_list)

 t : thread := dequeue(ready_list)

 转运(吨)

 transfer(t)

在调用调度程序之前,想要在未来某个时间点再次运行的线程必须将其自己的上下文块放入某个适当的数据结构中。如果它为了公平起见而阻塞(以便让其他线程有机会运行),那么它会将其上下文块排入就绪列表:

Before calling into the scheduler, a thread that wants to run again at some point in the future must place its own context block in some appropriate data structure. If it is blocking for the sake of fairness—to give some other thread a chance to run—then it enqueues its context block on the ready list:

程序产量()

procedure yield()

 入队(就绪列表,当前线程)

 enqueue(ready_list, current_thread)

 重新安排()

 reschedule()

为了阻塞同步,线程将自身添加到与等待条件关联的队列中:

To block for synchronization, a thread adds itself to a queue associated with the awaited condition:

程序 sleep_on(参考 Q:线程队列)

procedure sleep_on(ref Q : queue of thread)

 入队(Q,当前线程)

 enqueue(Q, current_thread)

 重新安排()

 reschedule()

当正在运行的线程执行使条件成立的操作时,它会从关联队列中删除一个或多个线程,并将它们排入就绪列表。■

When a running thread performs an operation that makes a condition true, it removes one or more threads from the associated queue and enqueues them on the ready list. ■

每当一个线程运行很长时间而其他线程处于可运行状态时,公平性就会成为一个问题。为了营造并发活动的假象,即使在单处理器上,我们也需要确保每个线程都能频繁地获得处理器的“切片”。对于协作式多线程,任何长时间运行的线程都必须不时地明确让出其核心(例如,在循环顶部),以允许其他线程运行。如第 13.1.1 节所述,这种方法允许一个编写不当的线程独占系统。即使使用正确编写的线程,由于不同线程之间让出时间不一致,也会导致不完美的公平性。

Fairness becomes an issue whenever a thread may run for a significant amount of time while other threads are runnable. To give the illusion of concurrent activity, even on a uniprocessor, we need to make sure that each thread gets a frequent “slice” of the processor. With cooperative multithreading, any long-running thread must yield its core explicitly from time to time (e.g., at the tops of loops), to allow other threads to run. As noted in Section 13.1.1, this approach allows one improperly written thread to monopolize the system. Even with properly written threads, it leads to less than perfect fairness due to nonuniform times between yields in different threads.

抢占

Preemption

理想情况下,我们希望公平地、相对精细地(即每秒多次)复用每个核心,而无需线程显式调用yield。在许多系统上,我们可以在语言实现中通过使用定时器信号进行抢占式多线程来实现这一点。在线程之间切换时,我们要求操作系统(可以访问硬件时钟)在未来的指定时间向当前正在运行的进程发送信号。操作系统通过保存进程的上下文(寄存器和pc)并将控制权转移到语言运行时系统中先前指定的处理程序例程来传递信号,如第9.6.1节所述。调用时,处理程序会修改当前正在运行的线程的状态,使其看起来好像线程刚刚执行了对标准yield例程的调用,并且即将执行其序言。然后,处理程序“返回”到yield,将控制权转移到其他线程,就好像正在运行的线程自愿放弃了对进程的控制一样。

Ideally, we should like to multiplex each core fairly and at a relatively fine grain (i.e., many times per second) without requiring that threads call yield explicitly. On many systems we can do this in the language implementation by using timer signals for preemptive multithreading. When switching between threads we ask the operating system (which has access to the hardware clock) to deliver a signal to the currently running process at a specified time in the future. The OS delivers the signal by saving the context (registers and pc) of the process and transferring control to a previously specified handler routine in the language run-time system, as described in Section 9.6.1. When called, the handler modifies the state of the currently running thread to make it appear that the thread had just executed a call to the standard yield routine, and was about to execute its prologue. The handler then “returns” into yield, which transfers control to some other thread, as if the one that had been running had relinquished control of the process voluntarily.

例 13.22

Example 13.22

抢占式多线程中的竞争条件

A race condition in preemptive multithreading

不幸的是,信号可能在任意时间到达,这会导致对调度程序的自愿调用和抢占触发的自动调用之间的竞争。为了说明这个问题,假设当前正在运行的进程刚刚将当前正在运行的线程入队到yield中的就绪列表中,并且即将调用reschedule时,信号到达。当信号处理程序“返回”到yield时,该进程将第二次将当前线程放入就绪列表中。如果在未来的某个时间点线程因同步而阻塞,则其在就绪列表中的第二个条目可能会导致它立即再次运行,而此时它应该在等待。如果信号发生在入队中间,此时就绪列表甚至不是一个结构正确的队列,则可能会出现更严重的问题。为了解决竞争并避免就绪列表损坏,线程包通常会在调度程序调用期间禁用信号传送:

Unfortunately, the fact that a signal may arrive at an arbitrary time introduces a race between voluntary calls to the scheduler and the automatic calls triggered by preemption. To illustrate the problem, suppose that a signal arrives when the currently running process has just enqueued the currently running thread onto the ready list in yield, and is about to call reschedule. When the signal handler “returns” into yield, the process will put the current thread into the ready list a second time. If at some point in the future the thread blocks for synchronization, its second entry in the ready list may cause it to run again immediately, when it should be waiting. Even worse problems can arise if a signal occurs in the middle of an enqueue, at a moment when the ready list is not even a properly structured queue. To resolve the race and avoid corruption of the ready list, thread packages commonly disable signal delivery during scheduler calls:

程序产量()

procedure yield()

 禁用信号()

 disable_signals()

 入队(就绪列表,当前线程)

 enqueue(ready_list, current_thread)

 重新安排()

 reschedule()

 重新启用信号()

 reenable_signals()

为了使此约定起作用,调用reschedule的每个代码片段都必须在调用之前禁用信号,并且必须在调用之后重新启用它们。(回想一下,类似的机制用于保护主程序和(请参见第 9.6.1 节中的事件处理程序。)在这种情况下,因为reschedule包含对transfer的调用,所以信号可能在一个线程中被禁用,而在另一个线程中被重新启用。■

For this convention to work, every fragment of code that calls reschedule must disable signals prior to the call, and must reenable them afterward. (Recall that a similar mechanism served to protect data shared between the main program and event handlers in Section 9.6.1.) In this case, because reschedule contains a call to transfer, signals maybe disabled in one thread and reenabled in another. ■

例 13.23

Example 13.23

上下文切换期间禁用信号

Disabling signals during context switch

事实证明,sleep_on例程还必须假设信号已被调用者禁用和启用。要了解原因,假设一个线程检查一个条件,发现它为假,然后调用sleep_on将自身挂起在与该条件关联的队列上。进一步假设在检查条件之后,但在调用sleep_on之前立即出现计时器信号。最后,假设允许在信号使条件为真之后运行的线程。由于第一个线程从未有机会将自己放入条件队列,因此第二个线程将找不到它以使其可运行。当第一个线程再次运行时,它将立即挂起自身,并且可能永远不会被唤醒。要关闭这个计时窗口(在这个时间间隔内,并发事件可能会损害程序的正确性),调用者必须确保在检查条件之前禁用信号:

It turns out that the sleep_on routine must also assume that signals are disabled and enabled by the caller. To see why, suppose that a thread checks a condition, finds that it is false, and then calls sleep_on to suspend itself on a queue associated with the condition. Suppose further that a timer signal occurs immediately after checking the condition, but before the call to sleep_on. Finally, suppose that the thread that is allowed to run after the signal makes the condition true. Since the first thread never got a chance to put itself on the condition queue, the second thread will not find it to make it runnable. When the first thread runs again, it will immediately suspend itself, and may never be awakened. To close this timing window—this interval in which a concurrent event may compromise program correctness—the caller must ensure that signals are disabled before checking the condition:

禁用信号()

disable_signals()

如果不是所需条件

if not desired_condition

 sleep_on(条件队列

 sleep_on(condition_queue)

重新启用信号

reenable_signals

在单处理器上,禁用信号可使检查和睡眠作为单个原子操作发生。■

On a uniprocessor, disabling signals allows the check and the sleep to occur as a single, atomic operation. ■

多处理器调度

Multiprocessor Scheduling

我们可以扩展抢占式线程包,使其在多个 OS 提供的进程上运行,方法是安排进程共享就绪列表和相关数据结构(条件队列等;请注意,每个进程必须具有单独的 current_thread变量)。如果进程在不同的物理核心上运行,则将能够同时运行多个线程。如果进程共享单个核心,那么即使操作系统中除一个进程外所有进程都被阻止,程序也将能够向前推进。任何可运行的线程都放置在就绪列表中,它将成为应用程序任何进程的执行候选。当进程调用reschedule时,我们在示例中使用的基于队列的就绪列表将为其提供等待时间最长的线程。更复杂的调度程序的就绪列表可能会优先考虑交互式或时间关键型线程,或者优先考虑上次在当前核心上运行的线程,因此可能仍在缓存中保留数据。

We can extend our preemptive thread package to run on top of more than one OS-provided process by arranging for the processes to share the ready list and related data structures (condition queues, etc.; note that each process must have a separate current_thread variable). If the processes run on different physical cores, then more than one thread will be able to run at once. If the processes share a single core, then the program will be able to make forward progress even when all but one of the processes are blocked in the operating system. Any thread that is runnable is placed in the ready list, where it becomes a candidate for execution by any of the application's processes. When a process calls reschedule, the queue-based ready list we have been using in our examples will give it the longest-waiting thread. The ready list of a more elaborate scheduler might give priority to interactive or time-critical threads, or to threads that last ran on the current core, and may therefore still have data in the cache.

正如抢占引入了对调度程序操作的自愿调用和自动调用之间的竞争一样,真正的或准并行性引入了不同 OS 进程中调用之间的竞争。为了解决竞争,我们必须实现额外的同步,以使不同进程中的调度程序操作具有原子性。我们将在第 13.3.4 节中回到这个主题。

Just as preemption introduced a race between voluntary and automatic calls to scheduler operations, true or quasiparallelism introduces races between calls in separate OS processes. To resolve the races, we must implement additional synchronization to make scheduler operations in separate processes atomic. We will return to this subject in Section 13.3.4.

13-01-9780124104099检查你的理解

Check Your Understanding

11. 解释协程、线程、轻量级进程重量级进程之间的区别。

11. Explain the differences among coroutines, threads, lightweight processes, and heavyweight processes.

12. 什么是准平行性

12. What is quasiparallelism?

13. 描述任务包编程模型。

13. Describe the bag of tasks programming model.

14. 什么是忙等待?它的主要替代方案是什么?

14. What is busy-waiting? What is its principal alternative?

15. 说出四种显式并发编程语言。

15. Name four explicitly concurrent programming languages.

16. 为什么消息传递程序不需要明确的同步机制?

16. Why don't message-passing programs require explicit synchronization mechanisms?

17. 基于语言和基于库的并发实现之间的权衡是什么?

17. What are the tradeoffs between language-based and library-based implementations of concurrency?

18.解释 数据并行任务并行之间的区别。

18. Explain the difference between data parallelism and task parallelism.

19. 描述并发程序中常用于创建新控制线程的六种不同机制。

19. Describe six different mechanisms commonly used to create new threads of control in a concurrent program.

20. 从什么意义上来说fork/join比co-begin更强大?

20. In what sense is fork/join more powerful than co-begin?

21. Java 中的 线程池是什么?它有什么用途?

21. What is a thread pool in Java? What purpose does it serve?

22. 什么是两级线程实现?

22. What is meant by a two-level thread implementation?

23. 什么是就绪清单

23. What is a ready list?

24. 描述在协程之上的调度、抢占和(真正的)并行的渐进实现。

24. Describe the progressive implementation of scheduling, preemption, and (true) parallelism on top of coroutines.

13.3 实现同步

13.3 Implementing Synchronization

如第 13.2.1 节所述,同步是共享内存并发程序的主要语义挑战。通常,同步用于使某些操作原子化或延迟该操作直到某些必要的先决条件成立。如第 13.1 节所述,原子性最常通过互斥锁实现。互斥确保在给定时间点只有一个线程正在执行某个关键代码段。关键代码段通常将共享数据结构从一种一致状态转换为另一种一致状态。

As noted in Section 13.2.1, synchronization is the principal semantic challenge for shared-memory concurrent programs. Typically, synchronization serves either to make some operation atomic or to delay that operation until some necessary precondition holds. As noted in Section 13.1, atomicity is most commonly achieved with mutual exclusion locks. Mutual exclusion ensures that only one thread is executing some critical section of code at a given point in time. Critical sections typically transform a shared data structure from one consistent state to another.

条件同步允许线程等待先决条件,通常表示为一个或多个共享变量中的值的谓词。人们很容易将互斥视为条件同步的一种形式(直到没有其他线程处于其临界区时才继续),但这种条件需要所有现有线程之间达成共识,而条件同步通常不提供这一点。

Condition synchronization allows a thread to wait for a precondition, often expressed as a predicate on the value(s) in one or more shared variables. It is tempting to think of mutual exclusion as a form of condition synchronization (don't proceed until no other thread is in its critical section), but this sort of condition would require consensus among all extant threads, something that condition synchronization doesn't generally provide.

我们的并行线程实现(在13.2.4 节末尾进行了概述)需要原子性和条件同步。就绪列表和相关数据结构上的操作的原子性确保它们始终满足一组逻辑不变量:列表格式正确,每个线程要么正在运行,要么恰好驻留在一个列表中,等等。条件同步出现在以下要求中:需要运行线程的进程必须等到就绪列表非空。

Our implementation of parallel threads, sketched at the end of Section 13.2.4, requires both atomicity and condition synchronization. Atomicity of operations on the ready list and related data structures ensures that they always satisfy a set of logical invariants: the lists are well formed, each thread is either running or resides in exactly one list, and so forth. Condition synchronization appears in the requirement that a process in need of a thread to run must wait until the ready list is nonempty.

值得强调的是,我们通常不希望过度同步程序。这样做会消除并行的机会,而为了提高性能,我们通常希望最大限度地提高并行性。此外,并非所有竞争都是坏的。如果两个进程正在竞争将最后一个线程从就绪列表中出队,我们通常不关心哪个成功,哪个等待另一个线程。我们关心的是出队的实现没有内部的指令级竞争,这可能会损害就绪列表的完整性。一般来说,我们的目标是仅提供必要的同步以消除“坏”竞争 - 否则可能会导致程序产生不正确的结果。

It is worth emphasizing that we do not in general want to overly synchronize programs. To do so would eliminate opportunities for parallelism, which we generally want to maximize in the interest of performance. Moreover not all races are bad. If two processes are racing to dequeue the last thread from the ready list, we don't generally care which succeeds and which waits for another thread. We do care that the implementation of dequeue does not have internal, instruction-level races that might compromise the ready list's integrity. In general, our goal is to provide only as much synchronization as is necessary to eliminate “bad” races— those that might otherwise cause the program to produce incorrect results.

在下面的第一小节中,我们考虑忙等待同步。在第二小节中,我们提出了一种称为非阻塞同步的替代方案,其中无需互斥即可实现原子性。在第三小节中,我们回到内存一致性的主题(最初在13.1.2 节中提到),并讨论其对语言级同步机制的语义和实现的影响。最后,在13.3.413.3.5节中,我们使用进程间的忙等待来实现并行安全的线程调度程序,然后依次使用此调度程序来实现最基本的基于调度程序的同步机制:即信号量。

In the first subsection below we consider busy-wait synchronization. In the second we present an alternative, called nonblocking synchronization, in which atomicity is achieved without the need for mutual exclusion. In the third subsection we return to the subject of memory consistency (originally mentioned in Section 13.1.2), and discuss its implications for the semantics and implementation of language-level synchronization mechanisms. Finally, in Sections 13.3.4 and 13.3.5, we use busy-waiting among processes to implement a parallelism-safe thread scheduler, and then use this scheduler in turn to implement the most basic scheduler-based synchronization mechanism: namely, semaphores.

13.3.1 忙等待同步

13.3.1 Busy-Wait Synchronization

如果我们可以把条件转换为“位置X包含值Y ”的形式,那么忙等待条件同步就很容易了:需要等待条件的线程可以简单地循环读取X ,等待Y出现。要等待涉及多个位置的条件,需要原子性来同时读取位置,但考虑到这一点,实现又是一个简单的循环。

Busy-wait condition synchronization is easy if we can cast a condition in the form of “location X contains value Y“: a thread that needs to wait for the condition can simply read X in a loop, waiting for Y to appear. To wait for a condition involving more than one location, one needs atomicity to read the locations together, but given that, the implementation is again a simple loop.

其他形式的忙等待同步稍微复杂一些。在本节的剩余部分,我们将讨论自旋锁(提供互斥)和屏障(确保所有线程都到达该点之前,任何线程都不会继续超过程序中的给定点)。

Other forms of busy-wait synchronization are somewhat trickier. In the remainder of this section we consider spin locks, which provide mutual exclusion, and barriers, which ensure that no thread continues past a given point in a program until all threads have reached that point.

自旋锁

Spin Locks

一般认为,Dekker 发现了第一个双线程互斥算法,该算法不需要除加载和存储之外的原子指令。Dijkstra [ Dij65 ] 于 1965 年发表了一个适用于n 个线程的版本。Peterson [ Pet81 ] 于 1981 年发表了一个简单得多的双线程算法。以 Peterson 算法为基础,可以构造一个分层的n线程锁,但是这需要O ( nlogn )空间和O (logn )时间才能让一个线程进入其临界区 [ YA93 ]。Lamport [ Lam87 ] 2于 1987 年发表了一个n线程算法,在没有锁竞争的情况下,该算法占用空间为O ( n ),时间是O (1)。不幸的是,当多个线程同时尝试进入其临界区时,该算法需要O ( n ) 时间。

Dekker is generally credited with finding the first two-thread mutual exclusion algorithm that requires no atomic instructions other than load and store. Dijkstra [Dij65] published a version that works for n threads in 1965. Peterson [Pet81] published a much simpler two-thread algorithm in 1981. Building on Peterson's algorithm, one can construct a hierarchical n-thread lock, but it requires O(n log n) space and O(log n) time to get one thread into its critical section [YA93]. Lamport [Lam87]2 published an n-thread algorithm in 1987 that takes O(n) space and O(1) time in the absence of competition for the lock. Unfortunately, it requires O(n) time when multiple threads attempt to enter their critical section at once.

例 13.24

Example 13.24

基本test_and_set

The basic test_and_set lock

虽然所有这些算法在历史上都很重要,但实际的自旋锁需要在恒定的时间和空间内运行,为此,需要一个原子指令,它的功能不仅仅是加载或存储。从 20 世纪 60 年代开始,硬件设计师开始为他们的处理器配备指令,这些指令可以将读取、修改和写入内存位置作为单个原子操作。最简单的此类指令称为test_and_set。它将布尔变量设置为true并返回该变量之前是否为false的指示。给定test_and_set,获取自旋锁几乎是微不足道的:

While all of these algorithms are historically important, a practical spin lock needs to run in constant time and space, and for this one needs an atomic instruction that does more than load or store. Beginning in the 1960s, hardware designers began to equip their processors with instructions that read, modify, and write a memory location as a single atomic operation. The simplest such instruction is known as test_and_set. It sets a Boolean variable to true and returns an indication of whether the variable was previously false. Given test_and_set, acquiring a spin lock is almost trivial:

while not test_and_set(L)

while not test_and_set(L)

 -— 无 — 旋转

 -— nothing -— spin

实际上,在循环中嵌入test_and_set往往会导致多核或多处理器机器上出现不可接受的通信量,因为缓存一致性机制会尝试协调多个试图获取锁的内核的写入。这种对硬件资源的过度需求被称为争用,是大型机器上实现良好性能的主要障碍。

In practice, embedding test_and_set in a loop tends to result in unacceptable amounts of communication on a multicore or multiprocessor machine, as the cache coherence mechanism attempts to reconcile writes by multiple cores attempting to acquire the lock. This overdemand for hardware resources is known as contention, and is a major obstacle to good performance on large machines.

例 13.25

Example 13.25

測試-並測試-並設置

Test-and-test_and_set

为了减少争用,同步库的编写者经常使用 test-and- test_and_set锁,它会旋转普通读取(由缓存满足),直到锁看起来空闲(参见图 13.8)。当线程释放锁时,仍然会有大量总线或互连活动,因为等待线程执行它们的test_and_sets,但至少这种活动只发生在临界区边界。在大型机器上,可以通过实现退避策略进一步减少争用,在该策略中,尝试获取锁失败的线程会等待一段时间再重试。■

To reduce contention, the writers of synchronization libraries often employ a test-and-test_and_set lock, which spins with ordinary reads (satisfied by the cache) until it appears that the lock is free (see Figure 13.8). When a thread releases a lock there still tends to be a flurry of bus or interconnect activity as waiting threads perform their test_and_sets, but at least this activity happens only at the boundaries of critical sections. On a large machine, contention can be further reduced by implementing a backoff strategy, in which a thread that is unsuccessful in attempting to acquire a lock waits for a while before trying again. ■

f13-08-9780124104099
图 13.8 一个简单的测试和test_and_set 。等待进程使用普通的读取(加载)指令旋转,直到锁看起来空闲,然后使用test_and_set来获取它。第一次访问是test_and_set,以便在常见(无竞争)情况下提高速度。

许多处理器提供比test_and_set更强大的原子指令。有些可以原子地交换寄存器和内存位置的内容。有些可以原子地将常量添加到内存位置,并返回先前的值。包括 x86、IA-64 和 SPARC 在内的多种处理器都提供了特别有用的指令,称为compare_and_swap (CAS)。此指令接受三个参数:位置、预期值和新值。它检查预期值是否出现在指定位置,如果是,则原子地用新值替换它。无论哪种情况,它都会返回是否进行了更改的指示。使用atomic_addcompare_and_swap之类的指令,可以构建公平的自旋锁,即保证线程按照它们第一次尝试的顺序获取锁。还可以构建在任意大型机器上工作良好的锁 — 即使在释放时也不会发生争用 [ MCS91Sco13 ]。这些主题超出了本文的范围。(也许值得一提的是,公平是一把双刃剑:虽然从语义的角度来看公平可能是可取的,但它往往会破坏缓存局部性,并且与抢占的相互作用非常糟糕。)

Many processors provide atomic instructions more powerful than test_and_set. Some can swap the contents of a register and a memory location atomically. Some can add a constant to a memory location atomically, returning the previous value. Several processors, including the x86, the IA-64, and the SPARC, provide a particularly useful instruction called compare_and_swap (CAS). This instruction takes three arguments: a location, an expected value, and a new value. It checks to see whether the expected value appears in the specified location, and if so replaces it with the new value, atomically. In either case, it returns an indication of whether the change was made. Using instructions like atomic_add or compare_and_swap, one can build spin locks that are fair, in the sense that threads are guaranteed to acquire the lock in the order in which they first attempt to do so. One can also build locks that work well—with no contention, even at release time—on arbitrarily large machines [MCS91, Sco13]. These topics are beyond the scope of the current text. (It is perhaps worth mentioning that fairness is a two-edged sword: while it may be desirable from a semantic point of view, it tends to undermine cache locality, and interacts very badly with preemption.)

互斥锁的一个重要变体是读写锁[ CHP71 ]。读写锁可以识别如果多个线程希望读取同一个数据结构,它们可以同时读取而不会相互干扰。只有当一个线程想要写入数据结构时,我们才需要阻止其他线程同时读取或写入。大多数忙等待互斥锁都可以扩展为允许读者并发访问(参见练习 13.8)。

An important variant on mutual exclusion is the reader–writer lock [CHP71]. Reader–writer locks recognize that if several threads wish to read the same data structure, they can do so simultaneously without mutual interference. It is only when a thread wants to write the data structure that we need to prevent other threads from reading or writing simultaneously. Most busy-wait mutual exclusion locks can be extended to allow concurrent access by readers (see Exercise 13.8).

障碍

Barriers

数据并行算法通常由一系列高级步骤或阶段构成,通常表示为某个最外层循环的迭代。正确性通常取决于确保每个线程在进入下一步之前完成上一步。屏障用于提供这种同步。

Data-parallel algorithms are often structured as a series of high-level steps, or phases, typically expressed as iterations of some outermost loop. Correctness often depends on making sure that every thread completes the previous step before any moves on to the next. A barrier serves to provide this synchronization.

例 13.26

Example 13.26

有限元分析中的障碍

Barriers in finite element analysis

举一个具体的例子,有限元分析将物理对象(比如说一座桥梁)建模为大量微小碎片的集合。桥梁的每个碎片都会向其相邻的碎片施加力。重力对所有碎片施加向下的力。桥台对构成底板的碎片施加向上的力。风对表面碎片施加力。为了评估整个桥梁的应力(例如,评估其稳定性和抗故障能力),有限元程序可能会将碎片划分到大量线程中(可能每个核心一个)。从外力开始,程序将进行一系列迭代。在每次迭代中,每个线程都会根据上一次迭代中发现的力重新计算其碎片上的力。在迭代之间,线程将与屏障同步。如果在上一次迭代期间没有线程发现任何力发生显著变化,程序将停止。■

As a concrete example, finite element analysis models a physical object—a bridge, let us say—as an enormous collection of tiny fragments. Each fragment of the bridge imparts forces to the fragments adjacent to it. Gravity exerts a downward force on all fragments. Abutments exert an upward force on the fragments that make up base plates. The wind exerts forces on surface fragments. To evaluate stress on the bridge as a whole (e.g., to assess its stability and resistance to failures), a finite element program might divide the fragments among a large collection of threads (probably one per core). Beginning with the external forces, the program would then proceed through a sequence of iterations. In each iteration, each thread would recompute the forces on its fragments based on the forces found in the previous iteration. Between iterations, the threads would synchronize with a barrier. The program would halt when no thread found a significant change in any forces during the last iteration. ■

例 13.27

Example 13.27

“逆向思维”障碍

The “sense-reversing” barrier

实现忙等待屏障的最简单方法是使用全局共享计数器,由原子fetch_and_decrement指令修改。计数器从n(程序中的线程数)开始。当每个线程到达屏障时,它会减少计数器。如果它不是最后一个到达的线程,则线程会旋转布尔标志。最后一个线程(将计数器从 1 更改为 0 的线程)翻转布尔标志,允许其他线程继续。为了便于在连续迭代(称为屏障情节)中重用屏障数据结构,线程每次通过时都会等待标志的交替值。这个简单屏障的代码如图13.9所示。■

The simplest way to implement a busy-wait barrier is to use a globally shared counter, modified by an atomic fetch_and_decrement instruction. The counter begins at n, the number of threads in the program. As each thread reaches the barrier it decrements the counter. If it is not the last to arrive, the thread then spins on a Boolean flag. The final thread (the one that changes the counter from 1 to 0) flips the Boolean flag, allowing the other threads to proceed. To make it easy to reuse the barrier data structures in successive iterations (known as barrier episodes), threads wait for alternating values of the flag each time through. Code for this simple barrier appears in Figure 13.9. ■

f13-09-9780124104099
图 13.9 一个简单的“感知反转”屏障。每个线程都有自己的local_sense副本。线程共享 count 和 sense 的单个副本。

和简单的自旋锁一样,“反向感知”屏障在大型机器上会导致不可接受的争用水平。此外,对计数器的访问串行化意味着实现n线程屏障的时间是O ( n )。可以做得更好,但即使是最快的软件屏障也需要O (log n ) 时间来同步n 个线程 [ MCS91 ]。大型多处理器有时会提供特殊硬件来将此界限缩短到接近常数时间。

Like a simple spin lock, the “sense-reversing” barrier can lead to unacceptable levels of contention on large machines. Moreover the serialization of access to the counter implies that the time to achieve an n-thread barrier is O(n). It is possible to do better, but even the fastest software barriers require O(log n) time to synchronize n threads [MCS91]. Large multiprocessors sometimes provide special hardware to reduce this bound to close to constant time.

例 13.28

Example 13.28

Java 7 移相器

Java 7 phasers

Java 7 Phaser类提供了异常灵活的屏障支持。参与线程集可以从一个 Phaser 事件变为另一个 Phaser 事件。当数量很大时,Phaser 可以分层运行以对数时间运行。此外,到达和离开可以指定为单独的操作:在这两者之间,线程可以执行以下工作:(a) 不需要所有其他线程都已到达,(b) 不必在任何其他线程离开之前完成。■

The Java 7 Phaser class provides unusually flexible barrier support. The set of participating threads can change from one phaser episode to another. When the number is large, the phaser can be tiered to run in logarithmic time. Moreover, arrival and departure can be specified as separate operations: in between, a thread can do work that (a) does not require that all other threads have arrived, and (b) does not have to be completed before any other threads depart. ■

13.3.2 非阻塞算法

13.3.2 Nonblocking Algorithms

例 13.29

Example 13.29

使用CAS进行原子更新

Atomic update with CAS

当在关键段的开头获取锁并在结尾释放锁时,其他线程无法同时执行类似受保护的代码段。只要每个线程都遵循相同的约定,关键段内的代码就是原子的——它似乎同时发生。但这并不是实现原子性的唯一可能方法。假设我们希望对共享位置进行任意更新:

When a lock is acquired at the beginning of a critical section, and released at the end, no other thread can execute a similarly protected piece of code at the same time. As long as every thread follows the same conventions, code within the critical section is atomic—it appears to happen all at once. But this is not the only possible way to achieve atomicity. Suppose we wish to make an arbitrary update to a shared location:

x := foo(x);

x := foo(x);

请注意,此更新至少涉及对x 的两次访问:一次读取旧值,一次写入新值。我们可以使用锁来保护序列:

Note that this update involves at least two accesses to x: one to read the old value and one to write the new. We could protect the sequence with a lock:

获取(L)

acquire(L)

 r1:= x

 r1 := x

 r2 := foo(r1)——可能是多指令序列

 r2 := foo(r1)    -— probably a multi-instruction sequence

 x:= r2

 x := r2

释放(L)

release(L)

但是我们也可以在不使用锁的情况下使用compare_and_swap来做到这一点:

But we can also do this without a lock, using compare_and_swap:

开始:

start:

 r1:= x

 r1 := x

 r2 := foo(r1) –– 可能是多指令序列

 r2 := foo(r1)        –– probably a multi-instruction sequence

 r2 := CAS(x, r1, r2) – 如果 x 没有改变,则替换它

 r2 := CAS(x, r1, r2)   –– replace x if it hasn't changed

 如果!r2转到开始

 if !r2 goto start

如果多个核心同时执行此代码,则其中一个核心保证在循环的第一次执行中成功。其他核心将失败并重试。此示例说明CAS是用于单位置原子更新的通用原语。ARM 、MIPS 和 Power 处理器上有一个类似的原语,称为load_linked/store_conditional ;我们将在练习 13.7中考虑它。■

If several cores execute this code simultaneously, one of them is guaranteed to succeed the first time around the loop. The others will fail and try again. This example illustrates that CAS is a universal primitive for single-location atomic update. A similar primitive, known as load_linked/store_conditional, is available on ARM, MIPS, and Power processors; we consider it in Exercise 13.7. ■

到目前为止,我们在讨论中一直使用操作系统中“阻塞”的定义:阻塞的线程放弃核心而不是主动旋转。另一种定义来自并发算法理论。在这里,旋转和放弃核心之间的选择并不重要:如果线程在没有其他线程操作的情况下无法向前推进,则称该线程为“阻塞”。相反,如果在系统的每个可到达状态下,执行该操作的任何线程在自行运行(不受其他线程进一步干扰)的情况下都能保证在有限步骤内完成,则称该操作为非阻塞。

In our discussions thus far, we have used a definition of “blocking” that comes from operating systems: a thread that blocks gives up the core instead of actively spinning. An alternative definition comes from the theory of concurrent algorithms. Here the choice between spinning and giving up the core is immaterial: a thread is said to be “blocked” if it cannot make forward progress without action by other threads. Conversely, an operation is said to be nonblocking if in every reachable state of the system, any thread executing that operation is guaranteed to complete in a finite number of steps if it gets to run by itself (without further interference by other threads).

从这个理论意义上讲,无论实现方式如何,锁本质上都是阻塞的:如果一个线程持有锁,则其他需要该锁的线程都无法继续。相比之下,示例 13.29中基于CAS的代码是非阻塞的:如果CAS操作失败,那是因为其他线程已经取得了进展。此外,如果除了一个线程之外的所有线程都停止运行(例如由于抢占),则保证剩下的线程能够取得进展。

In this theoretical sense of the word, locks are inherently blocking, regardless of implementation: if one thread holds a lock, no other thread that needs that lock can proceed. By contrast, the CAS-based code of Example 13.29 is nonblocking: if the CAS operation fails, it is because some other thread has made progress. Moreover if all threads but one stop running (e.g., because of preemption), the remaining thread is guaranteed to make progress.

我们可以从示例 13.29中推广到设计各种无需锁即可操作的专用并发数据结构。对这些结构的修改通常(但并非总是)遵循以下模式

We can generalize from Example 13.29 to design a variety of special-purpose concurrent data structures that operate without locks. Modifications to these structures often (though not always) follow the pattern

重复

repeat

 准备

 prepare

 CAS——(或其他原子操作)

 CAS      –– (or some other atomic operation)

直至成功

until success

清理

clean up

如果读取多个位置,算法的“准备”部分可能需要进行双重检查,以确保没有任何值发生变化(即所有值均已一致读取),然后再转到 CAS。一旦此双重检查成功,只读操作可能只会返回。

If it reads more than one location, the “prepare” part of the algorithm may need to double-check to make sure that none of the values has changed (i.e., that all were read consistently) before moving on to the CAS. A read-only operation may simply return once this double-checking is successful.

示例 13.29的基于CAS 的更新中,算法的“准备”部分读取 × 的旧值并确定新值应该是什么;“清理”部分为空。在其他算法中可能会有大量清理工作。在所有情况下,正确性的关键在于:(1)如果需要重复,“准备”部分是无害的;(2)如果CAS成功,则在逻辑上以所有其他线程可见的方式完成操作;(3)如果需要,“清理”可以由任何线程在原始线程延迟时执行。为另一个线程的操作执行清理通常称为帮助

In the CAS-based update of Example 13.29, the “prepare” part of the algorithm reads the old value of × and figures out what the new value ought to be; the “clean up” part is empty. In other algorithms there may be significant cleanup. In all cases, the keys to correctness are that (1) the “prepare” part is harmless if we need to repeat; (2) the CAS, if successful, logically completes the operation in a way that is visible to all other threads; and (3) the “clean up,” if needed, can be performed by any thread if the original thread is delayed. Performing cleanup for another thread's operation is often referred to as helping.

例 13.30

Example 13.30

M&S 队列

The M&S queue

图 13.10说明了一个广泛使用的非阻塞并发队列。出队操作不需要清理,但入队操作需要。要将元素添加到队列末尾,线程会读取当前指针以查找队列中的最后一个节点,并使用CAS将该节点的下一个指针更改为指向新节点而不是为空。如果 CAS成功(没有其他线程已经更新相关的下一个指针),则新节点已插入。作为清理,必须更新指针以指向新节点,但任何线程都可以这样做 - 并且如果发现tail->next不为空,就会这样做。■

Figure 13.10 illustrates a widely used nonblocking concurrent queue. The dequeue operation does not require cleanup, but the enqueue operation does. To add an element to the end of the queue, a thread reads the current tail pointer to find the last node in the queue, and uses a CAS to change the next pointer of that node to point to the new node instead of being null. If the CAS succeeds (no other thread has already updated the relevant next pointer), then the new node has been inserted. As cleanup, the tail pointer must be updated to point to the new node, but any thread can do this—and will, if it discovers that tail->next is not null. ■

f13-10-9780124104099
图 13.10 非阻塞并发队列上的操作。在出队操作()中,单个CAS将头指针移至队列中的下一个节点。在入队操作()中,第一个CAS将尾节点的下一个指针更改为指向新节点,此时该操作在逻辑上已完成。任何线程都可以执行的后续“清理” CAS也会将尾指针移至指向新节点。

非阻塞算法比阻塞算法有几个优点。它们天生就容忍页面错误和抢占:如果一个线程在操作中途停止运行,它绝不会阻止其他线程继续执行。非阻塞算法还可以安全地用于信号(事件)和中断处理程序,从而避免出现示例 13.22中描述的问题。对于几个重要的数据结构和算法(包括堆栈、队列、排序列表、优先级队列、哈希表和内存管理),非阻塞算法也可以比锁更快。不幸的是,这些算法往往非常微妙且难以设计。它们主要用于语言级并发机制的实现和标准库包中。

Nonblocking algorithms have several advantages over blocking algorithms. They are inherently tolerant of page faults and preemption: if a thread stops running partway through an operation, it never prevents other threads from making progress. Nonblocking algorithms can also safely be used in signal (event) and interrupt handlers, avoiding problems like the one described in Example 13.22. For several important data structures and algorithms, including stacks, queues, sorted lists, priority queues, hash tables, and memory management, nonblocking algorithms can also be faster than locks. Unfortunately, these algorithms tend to be exceptionally subtle and difficult to devise. They are used primarily in the implementation of language-level concurrency mechanisms and in standard library packages.

13.3.3 内存一致性

13.3.3 Memory Consistency

到目前为止,在我们所有的讨论中,我们都隐含地依赖于硬件内存一致性。不幸的是,单靠一致性不足以使多处理器甚至单个多核处理器按照大多数程序员的期望运行。当多个位置几乎同时被写入时,我们还必须担心写入对不同核心可见的顺序。

In all our discussions so far, we have depended, implicitly, on hardware memory coherence. Unfortunately, coherence alone is not enough to make a multiprocessor or even a single multicore processor behave as most programmers would expect. We must also worry, when more than one location is written at about the same time, about the order in which the writes become visible to different cores.

直观地讲,大多数程序员都希望共享内存具有顺序一致性— 即所有写入操作都以相同的顺序对所有内核可见,并且任何给定内核的写入操作都以执行顺序可见。不幸的是,这种行为很难有效实现 — 难到大多数硬件设计人员根本不提供它。相反,他们提供了几种宽松的内存模型之一,其中某些加载和存储可能看起来“无序”发生。宽松一致性对语言设计人员、编译器编写人员以及同步机制和非阻塞算法的实现者具有重要影响。

Intuitively, most programmers expect shared memory to be sequentially consistent—to make all writes visible to all cores in the same order, and to make any given core's writes visible in the order they were performed. Unfortunately, this behavior turns out to be very hard to implement efficiently—hard enough that most hardware designers simply don't provide it. Instead, they provide one of several relaxed memory models, in which certain loads and stores may appear to occur “out of order.” Relaxed consistency has important ramifications for language designers, compiler writers, and the implementors of synchronization mechanisms and nonblocking algorithms.

订购成本

The Cost of Ordering

顺序一致性的基本问题在于,直接的实现需要硬件和编译器来序列化我们宁愿以任意顺序执行的操作。

The fundamental problem with sequential consistency is that straightforward implementations require both hardware and compilers to serialize operations that we would rather be able to perform in arbitrary order.

例 13.31

Example 13.31

写缓冲区和一致性

Write buffers and consistency

例如,考虑一下普通存储指令的实现。如果发生缓存未命中,该指令可能需要数百个周期才能完成。大多数处理器都被设计为继续执行,而不是等待存储“在后台”完成时,内核将执行后续指令。即使在 L1 缓存中尚不可见的存储(或在尚不可见的存储之后发生的存储)也保存在称为写入缓冲区的队列中。根据此缓冲区中的条目检查加载,因此内核始终可以看到自己的先前存储,并且顺序程序可以正确执行。

Consider, for example, the implementation of an ordinary store instruction. In the event of a cache miss, this instruction can take hundreds of cycles to complete. Rather than wait, most processors are designed to continue executing subsequent instructions while the store completes “in the background.” Stores that are not yet visible in even the L1 cache (or that occurred after a store that is not yet visible) are kept in a queue called the write buffer. Loads are checked against the entries in this buffer, so a core always sees its own previous stores, and sequential programs execute correctly.

但是考虑一个并发程序,其中线程A将一个标志(称为inspected)设置为 true,然后读取位置X。 大约在同一时间,线程B将X从 0更新为 1,然后读取标志。 如果B的读取显示尚未设置inspected ,程序员可能自然会认为A将读取X的新值(1) :毕竟,B在检查标志之前更新 X ,而 A 在读取 X 之前设置标志,因此 A 不可能已经读取了X。 但是,在大多数机器上,当 A 对inspected的写入仍在其写入缓冲区中时, A可以读取X 。 同样, B可以读取inspected ,而它对X的写入仍在其写入缓冲区中。 结果可能非常不直观:

But consider a concurrent program in which thread A sets a flag (call it inspected) to true and then reads location X. At roughly the same time, thread B updates X from 0 to 1 and then reads the flag. If B's read reveals that inspected has not yet been set, the programmer might naturally assume that A is going to read newvalue (1) for X: after all, B updates X before checking the flag, and A sets the flag before reading X, so A cannot have read X already. On most machines, however, A can read X while its write of inspected is still in its write buffer. Likewise, B can read inspected while its write of X is still in its write buffer. The result can be very unintuitive behavior:

u13-01-9780124104099

按程序顺序, Ainspected的写入先于对X读取。按程序顺序,B对X的写入先于对inspected的读取。B对inspected的读取似乎先于A对inspected的写入,因为它看到的是未设置的值。然而,A对X的读取似乎也先于B对X的写入,因此xA = 0ib = false

A's write of inspected precedes its read of X in program order. B's write of X precedes its read of inspected in program order. B's read of inspected appears to precede A's write of inspected, because it sees the unset value. And yet A's read of X appears to precede B's write of X as well, leaving us with xA = 0 and ib = false.

这种“时间循环”也可能是由标准编译器优化引起的。传统上,编译器可以自由地重新排序指令(在没有数据依赖性的情况下)以提高处理器管道的预期性能。在这个例子中,为AB生成代码的编译器(不考虑另一个)可以选择反转对已检查X的操作顺序,即使在没有硬件重新排序的情况下也会产生明显的时间循环。■

This sort of “temporal loop” may also be caused by standard compiler optimizations. Traditionally, a compiler is free to reorder instructions (in the absence of a data dependence) to improve the expected performance of the processor pipelines. In this example, a compiler that generates code for either A or B (without considering the other) may choose to reverse the order of operations on inspected and X, producing an apparent temporal loop even in the absence of hardware reordering. ■

使用栅栏和同步指令强制排序

Forcing Order with Fences and Synchronization Instructions

为了避免时间循环,并发语言和库的实现者通常必须使用特殊的同步内存隔离指令。这些指令会以一定的代价强制执行硬件通常无法保证的排序。3它们的存在还会抑制编译器中的指令重新排序。

To avoid temporal loops, implementors of concurrent languages and libraries must generally use special synchronization or memory fence instructions. At some expense, these force orderings not normally guaranteed by the hardware.3 Their presence also inhibits instruction reordering in the compiler.

示例 13.31中,AB都必须防止其读取绕过(在之前完成)逻辑上更早的写入。通常,这可以通过将读取或写入标识为同步指令(例如,通过在x86 上使用XCHG指令实现)或在它们之间插入隔离(例如,在 SPARC 上插入membar StoreLoad )来实现。

In Example 13.31, both A and B must prevent their read from bypassing (completing before) the logically earlier write. Typically this can be accomplished by identifying either the read or the write as a synchronization instruction (e.g., by implementing it with an XCHG instruction on the x86) or by inserting a fence between them (e.g., membar StoreLoad on the SPARC).

例 13.32

Example 13.32

分布式一致性

Distributed consistency

有时,如示例 13.31所示,使用同步或隔离指令就足以恢复直观行为。但是,其他情况需要对程序进行更重大的更改。图 13.11中显示了一个示例。核心AB分别写入位置XY。这两个位置都由核心CD读取。如果C在分布式内存机器中物理上靠近A ,而D靠近B ,并且如果一致性消息同时传播,我们必须考虑CD以相反的顺序看到写入的可能性,从而导致另一个时间循环。

Sometimes, as in Example 13.31, the use of synchronization or fence instructions is enough to restore intuitive behavior. Other cases, however, require more significant changes to the program. An example appears in Figure 13.11. Cores A and B write locations X and Y, respectively. Both locations are read by cores C and D. If C is physically close to A in a distributed memory machine, and D is close to B, and if coherence messages propagate concurrently, we must consider the possibility that C and D will see the writes in opposite orders, leading to another temporal loop.

f13-11-9780124104099
图 13.11 写入的并发传播。在某些机器上,并发写入可能以不同的顺序到达核心。箭头显示明显的时间顺序。这里C可能读取cy = 0cx = 1,而D读取dx = 0dy = 1

在出现此问题的机器上,隔离和同步指令可能不足以解决问题。语言或库实现者(甚至应用程序程序员)可能需要将XY的写入与(隔离的)写入括起来,写入某些公共位置,以确保其中一个原始写入在另一个开始之前完成。最直接的方法是将写入封闭在基于锁的临界区中。即便如此,可能仍需要采取额外措施来确保CD不会无序执行XY的读取。■

On machines where this problem arises, fences and synchronization instructions may not suffice to solve the problem. The language or library implementor (or even the application programmer) may need to bracket the writes of X and Y with (fenced) writes to some common location, to ensure that one of the original writes completes before the other starts. The most straightforward way to do this is to enclose the writes in a lock-based critical section. Even then, additional measures may be needed to ensure that the reads of X and Y are not executed out of order by either C or D. ■

简化语言层面的推理

Simplifying Language-Level Reasoning

对于在单核上运行的程序,无论底层管道和内存层次结构的复杂性如何,每个制造商都保证指令将按程序顺序出现:任何指令都会看到某些前一条指令的效果,也不会看到任何后续指令的效果。对于在多核或多处理器机器上运行的程序,制造商还保证在某些情况下,在一个核心上执行的指令将按顺序被其他核心上的指令看到。不幸的是,这些情况因机器而异。MIPS 和 PA-RISC 处理器的一些实现是顺序一致的,IBM 的 z 系列大型机也是如此:如果核心B上的负载看到写入的值通过在核心A上进行存储,那么,传递地,保证在核心A上存储之前的所有操作都发生在核心B上加载之后的所有操作之前。其他处理器和实现则更为宽松。特别是,大多数机器都允许示例 13.31中的循环。SPARC 和 x86 禁止示例 13.32中的循环(图 13.11),但 Power、ARM 和 Itanium 都允许它。

For programs running on a single core, regardless of the complexity of the underlying pipeline and memory hierarchy, every manufacturer guarantees that instructions will appear to occur in program order: no instruction will fail to see the effects of some prior instruction, nor will it see the effects of any subsequent instruction. For programs running on a muilticore or multiprocessor machine, manufacturers also guarantee, under certain circumstances, that instructions executed on one core will be seen, in order, by instructions on other cores. Unfortunately, these circumstances vary from one machine to another. Some implementations of the MIPS and PA-RISC processors were sequentially consistent, as are IBM's z-Series mainframe machines: if a load on core B sees a value written by a store on core A, then, transitively, everything before the store on A is guaranteed to have happened before everything after the load on B. Other processors and implementations are more relaxed. In particular, most machines admit the loop of Example 13.31. The SPARC and x86 preclude the loop of Example 13.32 (Figure 13.11), but the Power, ARM, and Itanium all allow it.

考虑到不同机器之间的差异,语言设计者该怎么做呢?答案是 Sarita Adve 首次提出的,现在已嵌入 Java、C++ 和(不太正式的)其他语言中,即定义一个内存模型来捕捉“正确同步”程序的概念,然后为所有此类程序提供顺序一致性的假象。实际上,内存模型构成了程序员和语言实现之间的契约:如果程序员遵循契约规则,实现将隐藏底层硬件的所有排序怪癖。

Given this variation across machines, what is a language designer to do? The answer, first suggested by Sarita Adve and now embedded in Java, C++, and (less formally) other languages, is to define a memory model that captures the notion of a “properly synchronized” program, and then provide the illusion of sequential consistency for all such programs. In effect, the memory model constitutes a contract between the programmer and the language implementation: if the programmer follows the rules of the contract, the implementation will hide all the ordering eccentricities of the underlying hardware.

在通常的表述中,内存模型区分“普通”变量访问和特殊同步访问;后者不仅包括锁获取和释放,还包括对用特殊同步关键字(Java 或 C# 中的volatile ,C++ 中的atomic)声明的任何变量的读写。跨核心操作的顺序完全基于同步访问。如果 (1)在单个线程中,A按程序顺序位于 C 之前( 2) AC是同步操作,并且语言定义表明A位于C之前;或 (3) 存在操作B使得A HB BB HB C ,则我们说操作A 发生在操作 C ( A HB C )之前。 图片 图片 图片

In the usual formulation, memory models distinguish between “ordinary” variable accesses and special synchronization accesses; the latter include not only lock acquire and release, but also reads and writes of any variable declared with a special synchronization keyword (volatile in Java or C#, atomic in C++). Ordering of operations across cores is based solely on synchronization accesses. We say that operation A happens before operation C (A HB C) if (1) A comes before C in program order in a single thread; (2) A and C are synchronization operations and the language definition says that A comes before C; or (3) there exists an operation B such that A HB B and B HB C.

如果两个普通访问发生在不同的线程中,它们引用相同的位置,并且其中至少有一个是写入,则称它们发生冲突。如果它们发生冲突并且没有排序,则称它们构成数据争用- 实现可能允许其中任何一个先发生,并且程序行为可能会因此而改变。根据这些定义,内存模型契约很简单:无数据争用程序的执行始终是顺序一致的。

Two ordinary accesses are said to conflict if they occur in different threads, they refer to the same location, and at least one of them is a write. They are said to constitute a data race if they conflict and they are not ordered—the implementation might allow either one to happen first, and program behavior might change as a result. Given these definitions, the memory model contract is straightforward: executions of data-race–free programs are always sequentially consistent.

在大多数情况下,互斥锁的获取顺序是在最近的释放之后。易失性(原子)变量的读取顺序是在存储读取值的写入之后。各种其他操作(例如,我们将在13.4.4 节中研究的事务)也可能有助于跨线程排序。

In most cases, an acquire of a mutual exclusion lock is ordered after the most recent prior release. A read of a volatile (atomic) variable is ordered after the write that stored the value that was read. Various other operations (e.g., the transactions we will study in Section 13.4.4) may also contribute to cross-thread ordering.

例 13.33

Example 13.33

使用volatile避免数据竞争

Using volatile to avoid a data race

图 13.12中显示了一个排序的简单示例,其中线程A设置变量initialized以指示线程B使用引用p是安全的。如果initialized未声明为volatile ,则A写入 true 和B终止循环读取之间将不存在跨线程排序。访问initializedp都将是数据争用。在底层,编译器可以自由地将true的写入移到p的初始化之前(请记住,线程通常是单独编译的,并且编译器没有明显的方法来判断对pinitialized 的写入是否相互关联)。类似地,在具有宽松硬件内存模型的机器上,处理器和内存系统可以自由地按任意顺序执行写入,而不管编译器在机器代码中指定的顺序如何。在B的核心上,编译器或硬件也有可能在确认initializedtrue之前读取pvolatile声明排除了所有这些不良可能性。

A simple example of ordering appears in Figure 13.12, where thread A sets variable initialized to indicate that it is safe for thread B to use reference p. If initialized had not been declared as volatile, there would be no cross-thread ordering between A's write of true and B's loop-terminating read. Access to both initialized and p would then be data races. Under the hood, the compiler would have been free to move the write of true before the initialization of p (remember, threads are often separately compiled, and the compiler has no obvious way to tell that the writes to p and initialized have anything to do with one another). Similarly, on a machine with a relaxed hardware memory model, the processor and memory system would have been free to perform the writes in either order, regardless of the order specified by the compiler in machine code. On B's core, it would also have been possible for either the compiler or the hardware to read p before confirming that initialized was true. The volatile declaration precludes all these undesirable possibilities.

f13-12-9780124104099
图 13.12 使用 volatile 标志保护初始化。此处将初始化标记为 volatile 可避免数据争用,并确保B在安全之前不会访问p 。

回到示例 13.31,我们可以通过将Xinspected都声明为volatile,或通过将对它们的访问包含在原子操作中(由锁获取和释放括起来),来避免时间循环。在示例 13.32中,XY上的volatile声明将再次足以确保顺序一致性,但成本可能略高:在某些机器上,编译器可能需要使用额外的锁或特殊指令来强制对不相交位置的写入进行全序。■

Returning to Example 13.31, we might avoid a temporal loop by declaring both X and inspected as volatile, or by enclosing accesses to them in atomic operations, bracketed by lock acquire and release. In Example 13.32, volatile declarations on X and Y will again suffice to ensure sequential consistency, but the cost may be somewhat higher: on some machines, the compiler may need to use extra locks or special instructions to force a total order among writes to disjoint locations. ■

同步竞争在多线程程序中很常见。它们是错误还是预期行为取决于应用程序。另一方面,数据竞争几乎总是程序错误。它们很难推理 - 而且很少有用 - 因此 C++ 内存模型完全禁止它们:给定一个具有数据竞争的程序,C++ 实现可以显示任何行为。Ada有类似的规则。相比之下,对于 Java,对嵌入式应用程序的重视促使语言设计者以能够保持底层虚拟机完整性的方式限制竞争程序的行为。包含数据竞争的 Java 程序必须继续遵循所有正常的语言规则,并且任何相对于唯一的先前写入无序的读取都必须返回一个值,该值可能是由某个先前写入到同一位置的写入或相对于读取无序的写入写入的。在讨论语言的同步机制之后,我们将在第 13.4.3 节中返回 Java 内存模型。

Synchronization races are common in multithreaded programs. Whether they are bugs or expected behavior depends on the application. Data races, on the other hand, are almost always program bugs. They are so hard to reason about— and so rarely useful—that the C++ memory model outlaws them altogether: given a program with a data race, a C++ implementation is permitted to display any behavior whatsoever. Ada has similar rules. For Java, by contrast, an emphasis on embedded applications motivated the language designers to constrain the behavior of racy programs in ways that would preserve the integrity of the underlying virtual machine. A Java program that contains a data race must continue to follow all the normal language rules, and any read that is not ordered with respect to a unique preceding write must return a value that might have been written by some previous write to the same location, or by a write that is unordered with respect to the read. We will return to the Java Memory Model in Section 13.4.3, after we have discussed the language's synchronization mechanisms.

13.3.4 调度器实现

13.3.4 Scheduler Implementation

例 13.34

Example 13.34

在进程上调度线程

Scheduling threads on processes

要实现用户级线程,OS 级进程必须同步对就绪列表和条件队列的访问,通常通过旋转的方式。图 13.13显示了一个简单的可重入线程调度程序(在第一个进程返回之前,第二个进程可以安全地“重新进入”该调度程序) 。与第 13.2.4 节中的代码一样,我们在进入调度程序代码之前禁用计时器信号,以保护就绪列表和条件队列免受进程及其自己的信号处理程序的并发访问。■

To implement user-level threads, OS-level processes must synchronize access to the ready list and condition queues, generally by means of spinning. Code for a simple reentrant thread scheduler (one that can be “reentered” safely by a second process before the first one has returned) appears in Figure 13.13. As in the code in Section 13.2.4, we disable timer signals before entering scheduler code, to protect the ready list and condition queues from concurrent access by a process and its own signal handler. ■

f13-13-9780124104099
图 13.13 简单可重入(并行安全)调度程序的一部分伪代码。每个进程都有自己的current_thread副本。有一个共享的scheduler_lock和一个ready_list。如果进程有专用核心,则low_level_lock可以是普通的自旋锁;否则它可以是“自旋然后让出”锁(图 13.14)。内部循环重新安排忙等待,直到就绪列表非空sleep_on的代码无法禁用计时器信号并自行获取调度程序锁,因为调用者需要测试条件,然后作为单个原子操作进行阻塞。

例 13.35

Example 13.35

线程调度中的竞争条件

A race condition in thread scheduling

我们的代码假设有一个“低级”锁 ( scheduler_lock ) 来保护整个调度程序。在将其上下文块保存在队列上(例如,在yieldsleep_on中)之前,线程必须获取调度程序锁。然后,它必须在从reschedule返回后释放锁。当然,因为reschedule调用transfer,所以锁通常会由一个线程(与禁用定时器的线程相同)获取信号)并由另一个(重新启用计时器信号的同一个)释放。yield 的代码可以自己实现同步,因为它的工作是自包含的。另一方面,sleep_on 的代码不能,因为线程通常必须检查条件并在必要时作为单个原子操作进行阻塞

Our code assumes a single “low-level” lock (scheduler_lock) that protects the entire scheduler. Before saving its context block on a queue (e.g., in yield or sleep_on), a thread must acquire the scheduler lock. It must then release the lock after returning from reschedule. Of course, because reschedule calls transfer, the lock will usually be acquired by one thread (the same one that disables timer signals) and released by another (the same one that reenables timer signals). The code for yield can implement synchronization itself, because its work is self-contained. The code for sleep_on, on the other hand, cannot, because a thread must generally check a condition and block if necessary as a single atomic operation:

禁用信号()

disable_signals()

获取锁(scheduler_lock)

acquire_lock(scheduler_lock)

如果不是所需条件

if not desired_condition

 sleep_on(条件队列

 sleep_on(condition_queue)

释放锁(调度锁)

release_lock(scheduler_lock)

重新启用信号()

reenable_signals()

如果将信号和锁定操作移到sleep_on内部,则可能会出现以下竞争:线程A检查条件并发现其为假;线程B使条件为真,但发现条件队列为空;线程A永远在条件队列上休眠。■

If the signal and lock operations were moved inside of sleep_on, the following race could arise: thread A checks the condition and finds it to be false; thread B makes the condition true, but finds the condition queue to be empty; thread A sleeps on the condition queue forever. ■

例 13.36

Example 13.36

“旋转然后让出”锁

A “spin-then-yield” lock

只要每个进程都在不同的核心上运行,自旋锁就足以作为保护就绪列表和条件队列的“低级”锁。但是,如我们在第13.2.1 节中所述,如果条件只能由使用我们正在旋转的核心的其他进程来使之成立,那么自旋就没有什么意义。如果我们知道我们在单处理器上运行,那么我们就不需要调度程序上的锁(只需要禁用信号)。但是,如果我们可能在单处理器上运行,或者在核心数少于进程数的多处理器上运行,那么我们必须准备好在无法获得锁时放弃核心。最简单的方法是使用“自旋后让出”锁,它首先由 Ousterhout [ Ous82 ] 提出。图 13.14显示了这种锁的一个简单示例。在多道程序机器上,当就绪列表为空时,可能还需要放弃重新调度中的核心:尽管当前应用程序的其他任何进程都无法执行任何操作,但如果我们允许操作系统将核心交给另一个应用程序的进程,则整体系统吞吐量可能会提高。■

A spin lock will suffice for the “low-level” lock that protects the ready list and condition queues, so long as every process runs on a different core. As we noted in Section 13.2.1, however, it makes little sense to spin for a condition that can only be made true by some other process using the core on which we are spinning. If we know that we're running on a uniprocessor, then we don't need a lock on the scheduler (just the disabled signals). If we might be running on a uniprocessor, however, or on a multiprocessor with fewer cores than processes, then we must be prepared to give up the core if unable to obtain a lock. The easiest way to do this is with a “spin-then-yield” lock, first suggested by Ousterhout [Ous82]. A simple example of such a lock appears in Figure 13.14. On a multiprogrammed machine, it might also be desirable to relinquish the core inside reschedule when the ready list is empty: though no other process of the current application will be able to do anything, overall system throughput may improve if we allow the operating system to give the core to a process from another application. ■

f13-14-9780124104099
图 13.14 一种简单的自旋然后让出锁,设计用于在可以进行多道程序设计的多处理器上执行(即,操作系统级进程可以被抢占)。如果无法在固定的短时间内获取锁,进程将调用操作系统调度程序来让出其核心并降低其优先级,以便允许其他进程(如果有)运行。希望在下次安排让出进程执行时锁可用。

在大型多处理器上,我们可以通过为每个条件队列使用单独的锁,为就绪列表使用另一个锁来提高并发性。但是,我们必须小心,确保一个进程不可能将线程放入条件队列(或就绪列表),而另一个进程不可能在第一个进程完成从该线程的转移之前尝试转移到该线程(参见练习 13.13)。

On a large multiprocessor we might increase concurrency by employing a separate lock for each condition queue, and another for the ready list. We would have to be careful, however, to make sure it wasn't possible for one process to put a thread into a condition queue (or the ready list) and for another process to attempt to transfer into that thread before the first process had finished transferring out of it (see Exercise 13.13).

基于调度程序的同步

Scheduler-Based Synchronization

忙等待同步的问题在于它会消耗处理器周期,因此这些周期无法用于其他计算。忙等待同步只有在以下情况下才有意义:(1) 当前核心没有更好的事情可做,或 (2) 预期等待时间小于将上下文切换到其他线程然后再切换回来所需的时间。为了确保在各种系统上都能获得可接受的性能,大多数并发编程语言都采用基于调度程序的同步机制,当正在运行的线程阻塞时,这些机制会切换到其他线程。

The problem with busy-wait synchronization is that it consumes processor cycles—cycles that are therefore unavailable for other computation. Busy-wait synchronization makes sense only if (1) one has nothing better to do with the current core, or (2) the expected wait time is less than the time that would be required to switch contexts to some other thread and then switch back again. To ensure acceptable performance on a wide variety of systems, most concurrent programming languages employ scheduler-based synchronization mechanisms, which switch to a different thread when the one that was running blocks.

在下一小节中,我们将讨论信号量,这是最常见的基于调度程序的同步形式。在第 13.4 节中,我们将讨论监视器、条件临界区和事务内存等更高级别的概念。在每种情况下,基于调度程序的同步机制都会从调度程序的就绪列表中删除等待线程,只有当等待条件为真(或可能为真)时才返回该线程。相比之下,自旋然后让出锁仍然是一种忙等待机制:当前运行的进程放弃核心,但仍保留在就绪列表中。每次锁似乎空闲时,它都会执行test_and_set操作,直到最终成功。值得注意的是,忙等待同步通常是“级别独立的”——可以根据需要将其视为同步线程、进程或核心。基于调度程序的同步是“级别相关的”——当在语言运行时系统中实现时,它特定于线程,当在操作系统中实现时,它特定于进程。

In the following subsection we consider semaphores, the most common form of scheduler-based synchronization. In Section 13.4 we consider the higher-level notions of monitors, conditional critical regions, and transactional memory. In each case, scheduler-based synchronization mechanisms remove the waiting thread from the scheduler's ready list, returning it only when the awaited condition is true (or is likely to be true). By contrast, a spin-then-yield lock is still a busy-wait mechanism: the currently running process relinquishes the core, but remains on the ready list. It will perform a test_and_set operation every time the lock appears to be free, until it finally succeeds. It is worth noting that busy-wait synchronization is generally “level-independent”—it can be thought of as synchronizing threads, processes, or cores, as desired. Scheduler-based synchronization is “level-dependent”—it is specific to threads when implemented in the language run-time system, or to processes when implemented in the operating system.

例 13.37

Example 13.37

缓冲区受限问题

The bounded buffer problem

我们将使用有界缓冲区抽象来说明各种基于调度程序的同步机制的语义。有界缓冲区是一个有限大小的并发队列,生产者线程向其中插入数据,消费者线程从中移除数据。缓冲区用于平衡两类线程相对进度率的波动,从而提高系统吞吐量。有界缓冲区的正确实现需要原子性和条件同步:前者确保没有线程在其他线程操作过程中看到处于不一致状态的缓冲区;后者强制消费者在缓冲区为空时等待,生产者在缓冲区已满时等待。■

We will use a bounded buffer abstraction to illustrate the semantics of various scheduler-based synchronization mechanisms. A bounded buffer is a concurrent queue of limited size into which producer threads insert data, and from which consumer threads remove data. The buffer serves to even out fluctuations in the relative rates of progress of the two classes of threads, increasing system throughput. A correct implementation of a bounded buffer requires both atomicity and condition synchronization: the former to ensure that no thread sees the buffer in an inconsistent state in the middle of some other thread's operation; the latter to force consumers to wait when the buffer is empty and producers to wait when the buffer is full. ■

13.3.5 信号量

13.3.5 Semaphores

信号量是最古老的基于调度器的同步机制。它们由 Dijkstra 在 20 世纪 60 年代中期描述 [ Dij68a ],并出现在 Algol 68 中。它们至今仍被广泛使用,特别是在基于库的并发实现中。

Semaphores are the oldest of the scheduler-based synchronization mechanisms. They were described by Dijkstra in the mid-1960s [Dij68a], and appear in Algol 68. They are still heavily used today, particularly in library-based implementations of concurrency.

例 13.38

Example 13.38

信号量的实现

Semaphore implementation

信号量基本上是一个具有两个相关操作PV的计数器。4调用V的线程原子地增加计数器。调用P的线程等待计数器为正数,然后减少计数器。我们通常要求信号量是公平的,即线程按照启动它们的顺序完成P操作。图 13.15显示了我们的调度程序操作中PV的实现。请注意,当V允许在P中等待的线程立即继续时,我们省略了匹配的增量和减量。 ■

A semaphore is basically a counter with two associated operations, P and V.4 A thread that calls V atomically increments the counter. A thread that calls P waits until the counter is positive and then decrements it. We generally require that semaphores be fair, in the sense that threads complete P operations in the order that they started them. Implementations of P and V in terms of our scheduler operations appear in Figure 13.15. Note that we have elided the matching increment and decrement when a V allows a thread that is waiting in P to continue right away. ■

f13-15-9780124104099
图 13.15 信号量操作,与图 13.13的调度程序代码一起使用。

计数器初始化为 1 且PV操作总是成对出现的信号量称为二进制信号量。它用作基于调度程序的互斥锁:P操作获取锁;V释放锁。更一般地,计数器初始化为k的信号量可用于仲裁对某些资源的k 个副本的访问。计数器在任何特定时间的值表示当前未使用的副本数。练习 13.19指出二进制信号量可用于实现通用信号量,因此两者具有相同的表达能力,即使便利性不相等。

A semaphore whose counter is initialized to one and for which P and V operations always occur in matched pairs is known as a binary semaphore. It serves as a scheduler-based mutual exclusion lock: the P operation acquires the lock; V releases it. More generally, a semaphore whose counter is initialized to k can be used to arbitrate access to k copies of some resource. The value of the counter at any particular time indicates the number of copies not currently in use. Exercise 13.19 notes that binary semaphores can be used to implement general semaphores, so the two are of equal expressive power, if not of equal convenience.

例 13.39

Example 13.39

带信号量的有界缓冲区

Bounded buffer with semaphores

图 13.16显示了基于信号量的有界缓冲区问题解决方案。它使用二进制信号量实现互斥,使用两个通用(或计数)信号量实现条件同步。练习 13.17考虑使用信号量构建n线程屏障。■

Figure 13.16 shows a semaphore-based solution to the bounded buffer problem. It uses a binary semaphore for mutual exclusion, and two general (or counting) semaphores for condition synchronization. Exercise 13.17 considers the use of semaphores to construct an n-thread barrier. ■

f13-16-9780124104099
图 13.16 基于信号量的有界缓冲区代码互斥二进制信号量保护数据结构正确。full_slots和empty_slots通用信号量确保在安全之前不会启动任何操作。

13-01-9780124104099检查你的理解

Check Your Understanding

25. 什么是互斥?什么是临界区

25. What is mutual exclusion? What is a critical section?

26. 操作具有原子性是什么意思?解释原子性和条件同步之间的区别。

26. What does it mean for an operation to be atomic? Explain the difference between atomicity and condition synchronization.

27.描述 test_and_set指令的行为。说明如何使用它来构建自旋锁。

27. Describe the behavior of a test_and_set instruction. Show how to use it to build a spin lock.

28.描述 compare_and_swap指令的行为。与test_and_set相比,它有哪些优势?

28. Describe the behavior of the compare_and_swap instruction. What advantages does it offer in comparison to test_and_set?

29. 解释读写锁与“普通”锁有何不同。

29. Explain how a reader–writer lock differs from an “ordinary” lock.

30. 什么是障碍?在哪些类型的程序中障碍很常见?

30. What is a barrier? In what types of programs are barriers common?

31. 算法非阻塞是什么意思非阻塞算法与基于锁的算法相比有哪些优势?

31. What does it mean for an algorithm to be nonblocking? What advantages do nonblocking algorithms have over algorithms based on locks?

32. 什么是顺序一致性?为什么它很难实现?

32. What is sequential consistency? Why is it difficult to implement?

33. 内存一致性模型提供了哪些信息?硬件级和语言级内存模型之间的关系是什么?

33. What information is provided by a memory consistency model? What is the relationship between hardware-level and language-level memory models?

34. 解释如何扩展抢占式单处理器调度程序,使其在多处理器上正常工作。

34. Explain how to extend a preemptive uniprocessor scheduler to work correctly on a multiprocessor.

35. 什么是自旋然后让出锁?

35. What is a spin-then-yield lock?

36. 什么是有界缓冲区

36. What is a bounded buffer?

37. 什么是信号量?它支持哪些操作?二进制信号量一般信号量有何不同?

37. What is a semaphore? What operations does it support? How do binary and general semaphores differ?

13.4 语言级构造

13.4 Language-Level Constructs

尽管信号量被广泛使用,但人们普遍认为它对于结构良好、可维护的代码来说太“低级”了。它们有两个主要问题。首先,由于它们的操作只是子程序调用,因此很容易遗漏一个(例如,在具有多个嵌套if语句的控制路径上)。其次,除非它们隐藏在抽象中,否则给定信号量的使用往往会分散在整个程序中,这使得很难为了软件维护而追踪它们。

Though widely used, semaphores are also widely considered to be too “low level” for well-structured, maintainable code. They suffer from two principal problems. First, because their operations are simply subroutine calls, it is easy to leave one out (e.g., on a control path with several nested if statements). Second, unless they are hidden inside an abstraction, uses of a given semaphore tend to get scattered throughout a program, making it difficult to track them down for purposes of software maintenance.

13.4.1 监视器

13.4.1 Monitors

监视器是 Dijkstra [ Dij72 ]建议使用的一种解决信号量问题的方法。Brinch Hansen [ Bri73 ] 对其进行了更彻底的开发,Hoare [ Hoa74 ] 在 20 世纪 70 年代初对其进行了形式化。监视器已被纳入至少 20 种语言中,其中最具影响力的可能是Concurrent Pascal [ Bri75 ]、Modula (1) [ Wir77b ] 和 Mesa [ LR80 ] 。5它们

Monitors were suggested by Dijkstra [Dij72] as a solution to the problems of semaphores. They were developed more thoroughly by Brinch Hansen [Bri73], and formalized by Hoare [Hoa74] in the early 1970s. They have been incorporated into at least a score of languages, of which Concurrent Pascal [Bri75], Modula (1) [Wir77b], and Mesa [LR80] were probably the most influential.5 They

也强烈影响了 Java 同步机制的设计,我们将在第 13.4.3 节中讨论这一点。

also strongly influenced the design of Java's synchronization mechanisms, which we will consider in Section 13.4.3.

监视器是一个具有操作、内部状态和多个条件变量的模块或对象。在给定的时间点,仅允许给定监视器的一个操作处于活动状态。调用繁忙监视器的线程会自动延迟,直到监视器空闲。任何操作都可以通过等待条件变量来代表其调用线程暂停自身。操作还可以向条件变量发出信号,在这种情况下,其中一个等待线程将恢复,通常是第一个等待的线程。

A monitor is a module or object with operations, internal state, and a number of condition variables. Only one operation of a given monitor is allowed to be active at a given point in time. A thread that calls a busy monitor is automatically delayed until the monitor is free. On behalf of its calling thread, any operation may suspend itself by waiting on a condition variable. An operation may also signal a condition variable, in which case one of the waiting threads is resumed, usually the one that waited first.

例 13.40

Example 13.40

有界缓冲区监视器

Bounded buffer monitor

由于监视器的操作(条目)会及时自动相互排斥,因此程序员无需承担正确使用PV操作的责任。此外,由于监视器是一种抽象,因此对封装数据的所有操作(包括同步)都集中在一个地方。图 13.17显示了基于监视器的有界缓冲区问题解决方案。值得强调的是,监视器条件变量与信号量不同。具体来说,它们没有“内存”:如果在信号发生时没有线程在等待条件,则信号无效。相比之下,信号量上的V操作会增加信号量的计数器,从而允许一些未来的P操作成功,即使现在没有等待。■

Because the operations (entries) of a monitor automatically exclude one another in time, the programmer is relieved of the responsibility of using P and V operations correctly. Moreover because the monitor is an abstraction, all operations on the encapsulated data, including synchronization, are collected together in one place. Figure 13.17 shows a monitor-based solution to the bounded buffer problem. It is worth emphasizing that monitor condition variables are not the same as semaphores. Specifically, they have no “memory”: if no thread is waiting on a condition at the time that a signal occurs, then the signal has no effect. By contrast a V operation on a semaphore increments the semaphore's counter, allowing some future P operation to succeed, even if none is waiting now. ■

f13-17-9780124104099
图 13.17 基于监视器的有界缓冲区代码。插入和移除是入口子程序:它们需要独占访问监视器的数据。由于条件是无内存的,因此插入移除都可以安全地用信号结束其操作。

语义细节

Semantic Details

Hoare 对监视器的定义是,每个条件变量使用一个线程队列,外加两个簿记队列:入口队列紧急队列。试图进入繁忙监视器的线程在入口队列中等待。当线程在监视器内执行信号操作,而其他线程正在等待指定条件时,信号线程将在监视器的紧急队列中等待,而相应条件队列中的第一个线程将获得对监视器的控制权。如果没有线程在等待信号条件,则信号操作为无操作。当线程离开监视器时(通过完成其操作或等待条件),它将解除对紧急队列中的第一个线程的阻塞,或者,如果紧急队列为空,则解除对入口队列中的第一个线程的阻塞(如果有)。

Hoare's definition of monitors employs one thread queue for every condition variable, plus two bookkeeping queues: the entry queue and the urgent queue. A thread that attempts to enter a busy monitor waits in the entry queue. When a thread executes a signal operation from within a monitor, and some other thread is waiting on the specified condition, then the signaling thread waits on the monitor's urgent queue and the first thread on the appropriate condition queue obtains control of the monitor. If no thread is waiting on the signaled condition, then the signal operation is a no-op. When a thread leaves a monitor, either by completing its operation or by waiting on a condition, it unblocks the first thread on the urgent queue or, if the urgent queue is empty, the first thread on the entry queue, if any.

许多监视器实现都取消了紧急队列,或者对 Hoare 的原始定义进行了其他更改。从程序员的角度来看,两个主要的变化领域是信号操作的语义和当线程在两个或多个监视器调用的嵌套序列中wait时对互斥的管理。我们将在下面回到这些问题。

Many monitor implementations dispense with the urgent queue, or make other changes to Hoare's original definition. From the programmer's point of view, the two principal areas of variation are the semantics of the signal operation and the management of mutual exclusion when a thread waits inside a nested sequence of two or more monitor calls. We will return to these issues below.

监视器的正确性取决于维护监视器不变量。不变量是一个谓词,它捕获了“监视器的状态是一致的”这一概念。不变量最初和监视器退出时都需要为真。它还需要在每个等待语句中为真,并且在 Hoare 监视器中,在信号操作中也需要为真。对于我们的有界缓冲区示例,合适的不变量将断言full_slots正确指示缓冲区中的项目数,并且这些项目位于编号为next_fullnext_empty – 1 (mod SIZE ) 的插槽中。仔细检查图 13.17中的代码会发现不变量最初确实成立,并且任何时候我们修改不变量中提到的变量之一,我们总是在等待、发出信号或从条目返回之前相应地修改其他变量。

Correctness for monitors depends on maintaining a monitor invariant. The invariant is a predicate that captures the notion that “the state of the monitor is consistent.” The invariant needs to be true initially, and at monitor exit. It also needs to be true at every wait statement and, in a Hoare monitor, at signal operations as well. For our bounded buffer example, a suitable invariant would assert that full_slots correctly indicates the number of items in the buffer, and that those items lie in slots numbered next_full through next_empty – 1 (mod SIZE). Careful inspection of the code in Figure 13.17 reveals that the invariant does indeed hold initially, and that any time we modify one of the variables mentioned in the invariant, we always modify the others accordingly before waiting, signaling, or returning from an entry.

Hoare 用信号量来定义监视器。相反,用监视器来定义信号量也很容易(练习 13.18)。这两个定义结合起来证明了信号量和监视器同样强大:它们都可以表达彼此可以表达的所有同步形式。

Hoare defined his monitors in terms of semaphores. Conversely, it is easy to define semaphores in terms of monitors (Exercise 13.18). Together, the two definitions prove that semaphores and monitors are equally powerful: each can express all forms of synchronization expressible with the other.

信号作为暗示和绝对信息

Signals as Hints and Absolutes

例 13.41

Example 13.41

如何等待信号(提示或绝对)

How to wait for a signal (hint or absolute)

一般来说,当线程可能正在等待的某个条件变为真时,就会发出条件变量的信号。如果我们想保证线程唤醒时条件仍然为真,那么我们需要在信号发生时立即切换到该线程——因此需要紧急队列,并且需要确保信号操作中的监视器不变。实际上,在信号上切换上下文往往会带来不必要的调度开销:发出信号的线程在其操作的剩余时间内很少更改与信号相关的条件。为了减少开销,并消除确保监视器不变,Mesa 指定信号只是提示:语言运行时系统将一些等待线程移至就绪列表,但信号器保留对监视器的控制,而等待器必须在唤醒时重新检查条件。实际上,标准习语

In general, one signals a condition variable when some condition on which a thread may be waiting has become true. If we want to guarantee that the condition is still true when the thread wakes up, then we need to switch to the thread as soon as the signal occurs—hence the need for the urgent queue, and the need to ensure the monitor invariant at signal operations. In practice, switching contexts on a signal tends to induce unnecessary scheduling overhead: a signaling thread seldom changes the condition associated with the signal during the remainder of its operation. To reduce the overhead, and to eliminate the need to ensure the monitor invariant, Mesa specifies that signals are only hints: the language runtime system moves some waiting thread to the ready list, but the signaler retains control of the monitor, and the waiter must recheck the condition when it awakes. In effect, the standard idiom

如果不是所需条件

if not desired_condition

 等待(条件变量

 wait (condition_variable)

在 Hoare 监视器中,在 Mesa 中变为以下内容:

in a Hoare monitor becomes the following in Mesa:

虽然不符合要求

while not desired_condition

 等待(条件变量

 wait(condition_variable)

设计与实现

Design & Implementation

13.5 监视器信号语义

13.5 Monitor signal semantics

通过指定信号是提示而不是绝对的,Mesa 和 Modula-3(以及类似的 Java 和 C#,我们将在第 13.4.3 节中讨论)避免了立即从信号发送者切换到等待线程的需要。它们还允许更简单但效率更低的实现,这些实现缺乏信号和线程队列之间的一一对应关系,或者不一定保证等待线程在信号发生后将首先在其监视器中运行。但是,如果我们想确保在信号后始终有适当的线程运行,这种方法可能会导致复杂情况。假设唤醒的线程重新检查其条件并发现它仍然无法运行。如果可能还有其他线程可以运行,则错误唤醒的线程可能需要在再次等待之前重新发出条件信号:

By specifying that signals are hints, instead of absolutes, Mesa and Modula-3 (and similarly Java and C#, which we consider in Section 13.4.3) avoid the need to perform an immediate context switch from a signaler to a waiting thread. They also admit simpler, though less efficient implementations that lack a one-to-one correspondence between signals and thread queues, or that do not necessarily guarantee that a waiting thread will be the first to run in its monitor after the signal occurs. This approach can lead to complications, however, if we want to ensure that an appropriate thread always runs in the wake of a signal. Suppose an awakened thread rechecks its condition and discovers that it still can't run. If there may be some other thread that could run, the erroneously awakened thread may need to resignal the condition before it waits again:

如果不是所需条件

if not desired_condition

 环形

 loop

  等待(条件变量

  wait (condition_variable)

  如果需要

  if desired_condition

   休息

   break

  信号(条件变量

  signal (condition_variable)

实际上,信号会从一个线程“级联”到另一个线程,直到某个线程能够运行。(如果可能没有等待的线程能够运行,那么我们将需要额外的逻辑来在检查完每个线程后停止级联。)或者,使条件(可能)为真的线程可以使用信号操作的特殊广播版本来同时唤醒所有等待的线程。然后,每个线程将重新检查条件,并在适当的情况下再次等待,而无需显式级联。在任一情况下(级联信号或广播),信号作为提示都会以最坏情况下的潜在高开销换取常见情况下的潜在低开销和可能更简单的实现。

In effect the signal “cascades” from thread to thread until some thread is able to run. (If it is possible that no waiting thread will be able to run, then we will need additional logic to stop the cascading when every thread has been checked.) Alternatively, the thread that makes a condition (potentially) true can use a special broadcast version of the signal operation to awaken all waiting threads at once. Each thread will then recheck the condition and if appropriate wait again, without the need for explicit cascading. In either case (cascading signals or broadcast), signals as hints trade potentially high overhead in the worst case for potentially low overhead in the common case and a potentially simpler implementation.

Modula-3 采用了类似的方法。Concurrent Pascal 中出现了另一种方法,它指定信号操作会导致它出现的监视器操作立即返回。此规则保持较低的开销,并保留不变式,但排除了线程在发出条件信号后在监视器中执行有用工作的算法。■

Modula-3 takes a similar approach. An alternative appears in Concurrent Pascal, which specifies that a signal operation causes an immediate return from the monitor operation in which it appears. This rule keeps overhead low, and also preserves invariants, but precludes algorithms in which a thread does useful work in a monitor after signaling a condition. ■

嵌套监控调用

Nested Monitor Calls

在大多数监视器语言中,在嵌套的监视器操作序列中等待将释放最内层监视器上的互斥,但会使外层监视器保持锁定状态。如果另一个线程到达相应信号操作的唯一方法是通过相同的外层监视器,则这种情况可能会导致死锁。通常,我们使用术语“死锁”来描述线程集合都在等待彼此,并且它们都无法继续的任何情况。在这种特定情况下,首先进入外层监视器的线程正在等待第二个线程执行信号操作;然而,第二个线程正在等待第一个线程离开监视器。

In most monitor languages, a wait in a nested sequence of monitor operations will release mutual exclusion on the innermost monitor, but will leave the outer monitors locked. This situation can lead to deadlock if the only way for another thread to reach a corresponding signal operation is through the same outer monitor(s). In general, we use the term “deadlock” to describe any situation in which a collection of threads are all waiting for each other, and none of them can proceed. In this specific case, the thread that entered the outer monitor first is waiting for the second thread to execute a signal operation; the second thread, however, is waiting for the first to leave the monitor.

另一种方法是,在内监视器中等待时释放外监视器上的排斥,这种方法被早期的几个单处理器监视器实现所采用,包括 Modula 的原始实现 [ Wir77a ]。然而,它有一个明显的语义缺陷:它要求监视器不变量不仅在监视器出口和(可能)信号操作时成立,而且在任何可能导致等待或(根据霍尔语义)嵌套监视器中的信号的子程序调用时也成立。程序员可能并不完全了解这些调用;它们在源代码中肯定没有在语法上加以区分。

The alternative—to release exclusion on outer monitors when waiting in an inner one—was adopted by several early monitor implementations for uniprocessors, including the original implementation of Modula [Wir77a]. It has a significant semantic drawback, however: it requires that the monitor invariant hold not only at monitor exit and (perhaps) at signal operations, but also at any subroutine call that may result in a wait or (with Hoare semantics) a signal in a nested monitor. Such calls may not all be known to the programmer; they are certainly not syntactically distinguished in the source.

设计与实现

Design & Implementation

13.6 嵌套监控问题

13.6 The nested monitor problem

在内部监视器中等待时保持外部监视器上的排斥可能会导致与信号线程的死锁,而释放这些外部监视器可能会导致类似的(如果更微妙的话)死锁。当等待线程被唤醒时,它必须重新获取内部和外部监视器上的排斥。最内层的监视器当然是可用的,因为匹配的信号发生在那里,但通常没有办法确保外部监视器中不相关的线程不会忙碌。此外,其中一个线程可能需要访问内部监视器才能完成其工作并释放外部监视器。如果我们坚持要求唤醒的线程在信号之后首先在内部监视器中运行,那么就会造成死锁。避免此问题的一种方法是在程序的所有监视器之间安排互斥。此解决方案严重限制了多处理器实现中的并发性,但在单处理器上可能是可以接受的。练习 13.21中介绍了一种更通用的解决方案。

While maintaining exclusion on outer monitor(s) when waiting in an inner one may lead to deadlock with a signaling thread, releasing those outer monitors may lead to similar (if a bit more subtle) deadlocks. When a waiting thread awakens it must reacquire exclusion on both inner and outer monitors. The innermost monitor is of course available, because the matching signal happened there, but there is in general no way to ensure that unrelated threads will not be busy in the outer monitor(s). Moreover one of those threads may need access to the inner monitor in order to complete its work and release the outer monitor(s). If we insist that the awakened thread be the first to run in the inner monitor after the signal, then deadlock will result. One way to avoid this problem is to arrange for mutual exclusion across all the monitors of a program. This solution severely limits concurrency in multiprocessor implementations, but may be acceptable on a uniprocessor. A more general solution is addressed in Exercise 13.21.

13.4.2 条件临界区域

13.4.2 Conditional Critical Regions

例 13.42

Example 13.42

原始 CCR 语法

Original CCR syntax

条件临界区 (CCR) 是信号量的另一种替代方案,由 Brinch Hansen 在监视器出现的同时提出 [ Bri73 ]。临界区是语法上分隔的临界区,其中允许代码访问受保护的变量。条件临界区还指定布尔条件,该条件必须为真,控制权才能进入该区域:

Conditional critical regions (CCRs) are another alternative to semaphores, proposed by Brinch Hansen at about the same time as monitors [Bri73]. A critical region is a syntactically delimited critical section in which code is permitted to access a protected variable. A conditional critical region also specifies a Boolean condition, which must be true before control will enter the region:

区域protected_variable,当Boolean_condition执行时

region protected_variable, when Boolean_condition do

末端区域

end region

除了在该变量的区域语句内之外,任何线程都无法访问受保护的变量,并且到达区域语句的任何线程都会等待,直到条件为真并且当前没有其他线程位于同一变量的区域内。区域可以嵌套,但与嵌套监视器调用一样,程序员需要担心死锁。图 13.18使用 CCR 来实现有界缓冲区。■

No thread can access a protected variable except within a region statement for that variable, and any thread that reaches a region statement waits until the condition is true and no other thread is currently in a region for the same variable. Regions can nest, though as with nested monitor calls, the programmer needs to worry about deadlock. Figure 13.18 uses CCRs to implement a bounded buffer. ■

f13-18-9780124104099
图 13.18 有界缓冲区的条件临界区域。区域语句上的布尔条件消除了对显式条件变量的需要。

条件临界区出现在并发语言 Edison [ Bri81 ] 中,并且似乎还影响了 Ada 95 和 Java/C# 的同步机制。可以说这些后来的语言融合了监视器和 CCR 的功能,尽管方式不同。

Conditional critical regions appeared in the concurrent language Edison [Bri81], and also seem to have influenced the synchronization mechanisms of Ada 95 and Java/C#. These later languages might be said to blend the features of monitors and CCRs, albeit in different ways.

Ada 95 中的同步

Synchronization in Ada 95

Ada 中同步的主要机制是在 Ada 83 中引入的,它基于消息传递;我们将在第 13.5 节中描述它。Ada 95 通过受保护对象的概念增强了此机制。受保护对象可以有三种类型的方法:函数、过程和条目。函数只能读取对象的字段;过程和条目可以读取和写入它们。受保护对象上的隐式读写锁可确保潜在冲突的操作在时间上相互排斥:过程或条目获得对对象的独占访问权限;函数可以与其他函数同时操作,但不能与过程或条目同时操作。

The principal mechanism for synchronization in Ada, introduced in Ada 83, is based on message passing; we will describe it in Section 13.5. Ada 95 augments this mechanism with a notion of protected object. A protected object can have three types of methods: functions, procedures, and entries. Functions can only read the fields of the object; procedures and entries can read and write them. An implicit reader–writer lock on the protected object ensures that potentially conflicting operations exclude one another in time: a procedure or entry obtains exclusive access to the object; a function can operate concurrently with other functions, but not with a procedure or entry.

设计与实现

Design & Implementation

13.7 条件临界区域

13.7 Conditional critical regions

条件临界区避免了信号语义问题,因为它们使用显式布尔条件而不是条件变量,并且只能在临界区开始时等待条件。同时,它们也可能导致严重的效率低下。一般情况下,用于退出条件临界区的代码必须暂时恢复每个等待线程,允许该线程在其自己的引用环境中重新检查其条件。在某些特殊情况下可以进行优化(例如,对于仅依赖于全局变量或仅由单个布尔变量组成的条件),但在最坏的情况下,可能需要在每次退出区域时在每个等待线程中执行上下文切换。

Conditional critical regions avoid the question of signal semantics, because they use explicit Boolean conditions instead of condition variables, and because conditions can be awaited only at the beginning of critical regions. At the same time, they introduce potentially significant inefficiency. In the general case, the code used to exit a conditional critical region must tentatively resume each waiting thread, allowing that thread to recheck its condition in its own referencing environment. Optimizations are possible in certain special cases (e.g., for conditions that depend only on global variables, or that consist of only a single Boolean variable), but in the worst case it may be necessary to perform context switches in and out of every waiting thread on every exit from a region.

过程和条目在两个重要方面有所不同。首先,条目可以具有布尔表达式保护,调用任务(线程)将在开始执行之前等待该保护(与 CCR 的条件非常相似)。其次,条目支持三种特殊形式的调用:定时调用(在等待指定的时间后中止)、条件调用(如果调用无法立即进行,则执行替代代码)和异步调用(立即开始执行替代代码,但如果调用能够在替代代码完成之前继续进行,则中止该代码)。

Procedures and entries differ from one another in two important ways. First, an entry can have a Boolean expression guard, for which the calling task (thread) will wait before beginning execution (much as it would for the condition of a CCR). Second, an entry supports three special forms of call: timed calls, which abort after waiting for a specified amount of time, conditional calls, which execute alternative code if the call cannot proceed immediately, and asynchronous calls, which begin executing alternative code immediately, but abort it if the call is able to proceed before the alternative completes.

与 CCR 的条件相比,Ada 95 中受保护对象条目上的保护允许更高效的实现,因为它们不必在调用线程的上下文中进行评估。此外,由于所有保护都集中在受保护对象的定义中,因此编译器可以生成代码以尽可能高效地将它们作为一个组进行测试,这是 Kessels [ Kes77 ] 建议的方式。虽然 Ada 任务不能在条目中间等待条件(只能在开头等待),但它可以将自己重新排队到另一个条目上,从而实现大致相同的效果。有界缓冲区的 Ada 95 代码与图 13.18的伪代码非常相似;我们将细节留给练习 13.23

In comparison to the conditions of CCRs, the guards on entries of protected objects in Ada 95 admit a more efficient implementation, because they do not have to be evaluated in the context of the calling thread. Moreover, because all guards are gathered together in the definition of the protected object, the compiler can generate code to test them as a group as efficiently as possible, in a manner suggested by Kessels [Kes77]. Though an Ada task cannot wait on a condition in the middle of an entry (only at the beginning), it can requeue itself on another entry, achieving much the same effect. Ada 95 code for a bounded buffer would closely resemble the pseudocode of Figure 13.18; we leave the details to Exercise 13.23.

13.4.3 Java 中的同步

13.4.3 Synchronization in Java

例 13.43

Example 13.43

Java 中的synchronized语句

synchronized statement in Java

在 Java 中,每个可被多个线程访问的对象都有一个隐式的互斥锁,可以通过synchronized语句获取和释放:

In Java, every object accessible to more than one thread has an implicit mutual exclusion lock, acquired and released by means of synchronized statements:

同步(my_shared_obj){

synchronized (my_shared_obj) {

 … // 关键部分

 … // critical section

}

}

所有引用同一共享对象的同步语句的执行在时间上互相排斥。引用不同对象的同步语句可以并发进行。作为一种语法糖,类的方法可能以synchronized关键字为前缀,在这种情况下,方法的主体被视为已被隐式同步(this)语句包围。共享对象的非同步方法的调用(以及对公共字段的直接访问)可以彼此并发进行,也可以与synchronized语句或方法并发进行。■

All executions of synchronized statements that refer to the same shared object exclude one another in time. Synchronized statements that refer to different objects may proceed concurrently. As a form of syntactic sugar, a method of a class maybe prefixed with the synchronized keyword, in which case the body of the method is considered to have been surrounded by an implicit synchronized (this) statement. Invocations of nonsynchronized methods of a shared object— and direct accesses to public fields—can proceed concurrently with each other, or with a synchronized statement or method. ■

例 13.44

Example 13.44

在 Java 中以提示形式通知

notify as hint in Java

synchronized语句或方法中,线程可以通过调用预定义方法wait来挂起自身。Java中的wait没有参数:核心语言不区分线程在给定对象上挂起的不同原因(java.util.concurrent库在Java 5中成为标准,它提供了一种针对多种条件的机制;下面将对此进行详细介绍)。与Mesa一样,Java允许线程因虚假原因或在延迟后被唤醒;因此程序必须将wait的使用嵌入到条件测试循环中:

Within a synchronized statement or method, a thread can suspend itself by calling the predefined method wait. Wait has no arguments in Java: the core language does not distinguish among the different reasons why threads may be suspended on a given object (the java.util.concurrent library, which became standard with Java 5, does provide a mechanism for multiple conditions; more on this below). Like Mesa, Java allows a thread to be awoken for spurious reasons, or after a delay; programs must therefore embed the use of wait within a condition-testing loop:

while (!条件) {

while (! condition) {

 等待();

 wait();

}

}

调用对象的wait方法的线程会释放该对象的锁。但是,如果使用嵌套的synchronized语句或嵌套调用synchronized方法,则线程不会释放对任何其他对象的锁。■

A thread that calls the wait method of an object releases the object's lock. With nested synchronized statements, however, or with nested calls to synchronized methods, the thread does not release locks on any other objects. ■

要恢复在给定对象上挂起的线程,其他线程必须从引用同一对象的同步语句或方法中执行预定义方法通知。与wait 一样,通知没有参数。作为对通知调用的响应,语言运行时系统会选择在对象上挂起的任意线程并使其可运行。如果没有这样的线程,则通知无操作。与 Mesa 一样,有时唤醒给定对象中等待的所有线程可能是合适的;Java为此提供了内置的通知所有方法。

To resume a thread that is suspended on a given object, some other thread must execute the predefined method notify from within a synchronized statement or method that refers to the same object. Like wait, notify has no arguments. In response to a notify call, the language run-time system picks an arbitrary thread suspended on the object and makes it runnable. If there are no such threads, then the notify is a no-op. As in Mesa, it may sometimes be appropriate to awaken all threads waiting in a given object; Java provides a built-in notifyAll method for this purpose.

如果线程正在等待多个条件(即,如果它们的wait嵌入在不同的循环中),则无法保证“正确的”线程将被唤醒。为了确保适当的线程确实被唤醒,程序员可以选择使用notifyAll而不是notify。为了确保唤醒后只有一个线程继续运行,第一个发现其条件已被满足的线程满足条件的线程必须修改对象的状态,以便其他被唤醒的线程在运行时只需返回休眠状态即可。不幸的是,由于所有等待线程最终都会在每次其中一个线程可以运行时重新评估其条件,因此这种多条件问题的“解决方案”可能非常昂贵。

If threads are waiting for more than one condition (i.e., if their waits are embedded in dissimilar loops), there is no guarantee that the “right” thread will awaken. To ensure that an appropriate thread does wake up, the programmer may choose to use notifyAll instead of notify. To ensure that only one thread continues after wakeup, the first thread to discover that its condition has been satisfied must modify the state of the object in such a way that other awakened threads, when they get to run, will simply go back to sleep. Unfortunately, since all waiting threads will end up reevaluating their conditions every time one of them can run, this “solution” to the multiple-condition problem can be quite expensive.

C# 中的同步机制与刚才描述的 Java 机制类似。C# 中的lock语句与 Java 中的synchronized类似。它不能用于标记方法,但可以通过为方法指定Synchronized 属性来实现类似的效果(但有点笨拙) 。使用PulsePulseAll方法代替了notifynotifyAll

The mechanisms for synchronization in C# are similar to the Java mechanisms just described. The C# lock statement is similar to Java's synchronized. It cannot be used to label a method, but a similar effect can be achieved (a bit more clumsily) by specifying a Synchronized attribute for the method. The methods Pulse and PulseAll are used instead of notify and notifyAll.

锁定变量

Lock Variables

例 13.45

Example 13.45

Java 5 中的锁定变量

Lock variables in Java 5

在早期版本的 Java 中,关注效率的程序员通常需要设计一种算法,使线程在给定时间内永远不会等待给定对象中的多个条件。Java 5 中引入的java.util.concurrent包提供了更通用的解决方案。(C# 中也有类似的解决方案,但不在标准库中。)作为同步语句和方法的替代方案,现代 Java 程序员可以创建显式Lock变量。曾经可能编写的代码

In early versions of Java, programmers concerned with efficiency generally needed to devise algorithms in which threads were never waiting for more than one condition within a given object at a given time. The java.util.concurrent package, introduced in Java 5, provides a more general solution. (Similar solutions are possible in C#, but are not in the standard library.) As an alternative to synchronized statements and methods, modern Java programmers can create explicit Lock variables. Code that might once have been written

同步(my_shared_obj){

synchronized (my_shared_obj) {

 … // 关键部分

 … // critical section

}

}

设计与实现

Design & Implementation

13.8 Java 中的条件变量

13.8 Condition variables in Java

正如 Mesa 和 Java 所说明的那样,监视器和 CCR 之间的区别有些模糊。事实证明,可以(参见练习 13.22)通过以下方式完全解决一般同步问题:对于每个受保护的对象,只有一个布尔条件,线程会根据该条件旋转。然而,这些解决方案可能并不完美:它们相当于信号量的低级使用,没有同步语句和方法的隐式互斥。对于自然表达多个条件的程序,Java 的基本同步机制(以及 C# 中的类似机制)可能会迫使程序员在优雅和效率之间做出选择。Java 5 的并发增强功能是有意尝试减轻这种困境:变量保留了监视器和 CCR 的互斥和条件同步之间的区别,同时允许程序员将等待线程划分为可以独立唤醒的等价类。通过改变划分的精细度,程序员基本上可以选择 CCR 的简单性和 Hoare 式监视器的效率之间的任何一点。练习 13.2413.26使用有界缓冲区作为运行示例进一步探讨了这个问题。

As illustrated by Mesa and Java, the distinction between monitors and CCRs is somewhat blurry. It turns out to be possible (see Exercise 13.22) to solve completely general synchronization problems in such a way that for every protected object there is only one Boolean condition on which threads ever spin. The solutions, however, may not be pretty: they amount to low-level use of semaphores, without the implicit mutual exclusion of synchronized statements and methods. For programs that are naturally expressed with multiple conditions, Java's basic synchronization mechanism (and the similar mechanism in C#) may force the programmer to choose between elegance and efficiency. The concurrency enhancements of Java 5 were a deliberate attempt to lessen this dilemma: Lock variables retain the distinction between mutual exclusion and condition synchronization characteristic of both monitors and CCRs, while allowing the programmer to partition waiting threads into equivalence classes that can be awoken independently. By varying the fineness of the partition the programmer can choose essentially any point on the spectrum between the simplicity of CCRs and the efficiency of Hoare-style monitors. Exercises 13.24 through 13.26 explore this issue further using bounded buffers as a running example.

现在可以写成

may now be written

锁 l = new ReentrantLock();

Lock l = new ReentrantLock();

l. 锁();

l.lock();

尝试 {

try {

 … // 关键部分

 … // critical section

} 最后 {

} finally {

 l.解锁();

 l.unlock();

}

}

类似的接口支持读写锁。■

A similar interface supports reader–writer locks. ■

例 13.46

Example 13.46

Java 5 中的多重条件

Multiple Conditions in Java 5

与信号量一样,Java Lock变量缺少与同步语句和方法相关的在作用域结束时的隐式释放。显式释放的需要引入了潜在的错误源,但允许程序员创建以非 LIFO 顺序获取和释放锁的算法(参见示例 13.14 )。Java Lock变量以类似于Ada 95 的定时进入调用的方式还支持tryLock方法,该方法仅在锁立即可用或在可选指定的超时间隔内(布尔返回值表示尝试是否成功)时才获取锁。最后,Lock变量可以具有任意数量的关联Condition变量,从而可以轻松编写线程等待多个条件的算法,而无需诉诸notifyAll

Like semaphores, Java Lock variables lack the implicit release at end of scope associated with synchronized statements and methods. The need for an explicit release introduces a potential source of bugs, but allows programmers to create algorithms in which locks are acquired and released in non-LIFO order (see Example 13.14). In a manner reminiscent of the timed entry calls of Ada 95, Java Lock variables also support a tryLock method, which acquires the lock only if it is available immediately, or within an optionally specified timeout interval (a Boolean return value indicates whether the attempt was successful). Finally, a Lock variable may have an arbitrary number of associated Condition variables, making it easy to write algorithms in which threads wait for multiple conditions, without resorting to notifyAll:

条件 c1 = l.newCondition();

Condition c1 = l.newCondition();

条件 c2 = l.newCondition();

Condition c2 = l.newCondition();

c1.等待();

c1.await();

c2.信号();

c2.signal();

仅使用同步方法(无锁或同步语句)的 Java 对象与 Mesa 监视器非常相似,其中每个监视器限制一个条件变量(实际上,带有同步语句的对象有时在 Java 中称为监视器)。同样, Java 中以循环中的等待开始的同步语句类似于 CCR,其中已明确重新测试条件。由于通知也是明确的,因此 Java 实现不需要在每次退出临界区时重新评估条件(或唤醒明确这样做的线程),只需在发生通知时重新评估条件即可。

Java objects that use only synchronized methods (no locks or synchronized statements) closely resemble Mesa monitors in which there is a limit of one condition variable per monitor (and in fact objects with synchronized statements are sometimes referred to as monitors in Java). By the same token, a synchronized statement in Java that begins with a wait in a loop resembles a CCR in which the retesting of conditions has been made explicit. Because notify also is explicit, a Java implementation need not reevaluate conditions (or wake up threads that do so explicitly) on every exit from a critical section—only those in which a notify occurs.

Java 内存模型

The Java Memory Model

我们在13.3.3 节中介绍的 Java 内存模型明确指定了哪些操作可以保证在线程间按顺序执行。它还指定了对于程序执行中的每对读取和写入,是否允许读取返回写入写入的值。

The Java Memory Model, which we introduced in Section 13.3.3, specifies exactly which operations are guaranteed to be ordered across threads. It also specifies, for every pair of reads and writes in a program execution, whether the read is permitted to return the value written by the write.

通俗地说,Java 线程可以缓冲或重新排序其写入(在硬件或软件中),直到它写入一个volatile变量或留下一个监视器(释放锁、离开同步块或等待)。此时,其之前的所有写入都必须对其他线程可见。类似地,允许线程保留其他线程写入的值的缓存副本,直到它读取易失性变量或进入监视器(获取锁、进入同步块或从等待中唤醒)。此时,任何后续读取都必须获取其他线程写入的任何内容的新副本。

Informally, a Java thread is allowed to buffer or reorder its writes (in hardware or in software) until the point at which it writes a volatile variable or leaves a monitor (releases a lock, leaves a synchronized block, or waits). At that point all its previous writes must be visible to other threads. Similarly, a thread is allowed to keep cached copies of values written by other threads until it reads a volatile variable or enters a monitor (acquires a lock, enters a synchronized block, or wakes up from a wait). At that point any subsequent reads must obtain new copies of anything that has been written by other threads.

在没有线程内数据依赖的情况下,编译器可以自由地重新排序普通读取和写入。它还可以将普通读取和写入向下移动到后续volatile读取之后,向上移动到上一个volatile写入之后,或者从上方或下方移动到同步块中。它无法重新排序volatile访问、监视器入口或监视器出口。

The compiler is free to reorder ordinary reads and writes in the absence of intrathread data dependences. It can also move ordinary reads and writes down past a subsequent volatile read, up past a previous volatile write, or into a synchronized block from above or below. It cannot reorder volatile accesses, monitor entry, or monitor exit with respect to one another.

如果编译器可以证明在给定的时间间隔内,一个volatile变量或监视器不会被多个线程使用,那么它可以像普通访问一样重新排序其操作。对于无数据争用程序,这些规则可确保顺序一致性。此外,即使存在争用,Java 实现也能确保对象引用和 32 位及更小数量的读写始终是原子的,并且每次读取都会返回由某个无序写入或某个紧接在前的有序写入写入的值。

If the compiler can prove that a volatile variable or monitor isn't used by more than one thread during a given interval of time, it can reorder its operations like ordinary accesses. For data-race-free programs, these rules ensure the appearance of sequential consistency. Moreover even in the presence of races, Java implementations ensure that reads and writes of object references and of 32-bit and smaller quantities are always atomic, and that every read returns the value written either by some unordered write or by some immediately preceding ordered write.

事实证明,Java 内存模型的形式化是一项极其困难的任务。大部分困难源于希望为具有数据竞争的程序指定有意义的语义。C++11 内存模型(也在13.3.3 节中介绍)通过简单地禁止此类程序来避免这种复杂性。首先近似地,C++ 在内存访问上定义了一种先发生顺序,类似于 Java 中的顺序,然后保证所有冲突访问都按顺序排列的程序的顺序一致性。通过允许程序员在原子变量的单独读写上指定较弱的顺序,引入了适度的额外复杂性;我们在探索 13.42中考虑了这一特性。

Formalization of the Java memory model proved to be a surprisingly difficult task. Most of the difficulty stemmed from the desire to specify meaningful semantics for programs with data races. The C++11 memory model, also introduced in Section 13.3.3, avoids this complexity by simply prohibiting such programs. To first approximation, C++ defines a happens-before ordering on memory accesses, similar to the ordering in Java, and then guarantees sequential consistency for programs in which all conflicting accesses are ordered. Modest additional complexity is introduced by allowing the programmer to specify weaker ordering on individual reads and writes of atomic variables; we consider this feature in Exploration 13.42.

13.4.4 事务内存

13.4.4 Transactional Memory

我们考虑过的所有原子性通用机制(信号量、监视器、条件临界区)本质上都是锁的语法变体。需要相互排斥的临界区必须获取和释放相同的锁。相互独立的临界区只有在获取和释放单独的锁时才能并行运行。这给程序员带来了一个不幸的权衡:使用单个锁编写无数据争用程序很容易,但这样的程序无法扩展:随着核心和线程的增加,锁将成为瓶颈,程序性能将停滞不前。为了提高可扩展性,熟练的程序员将他们的程序数据划分为等价类,每个等价类都由单独的锁保护。然后,临界区必须获取每个访问的等价类的锁。如果不同的临界区以不同的顺序获取锁,则会导致死锁。然而,强制执行一个共同的顺序可能很困难,因为我们可能无法预测何时操作开始,最终需要访问哪些数据。更糟糕的是,正确性取决于锁定顺序这一事实意味着基于锁的程序片段无法组合:我们无法采用现有的基于锁的抽象并从新的关键部分中安全地调用它们。

All the general-purpose mechanisms we have considered for atomicity—semaphores, monitors, conditional critical regions—are essentially syntactic variants on locks. Critical sections that need to exclude one another must acquire and release the same lock. Critical sections that are mutually independent can run in parallel only if they acquire and release separate locks. This creates an unfortunate tradeoff for programmers: it is easy to write a data-race-free program with a single lock, but such a program will not scale: as cores and threads are added, the lock will become a bottleneck, and program performance will stagnate. To increase scalability, skillful programmers partition their program data into equivalence classes, each protected by a separate lock. A critical section must then acquire the locks for every accessed equivalence class. If different critical sections acquire locks in different orders, deadlock can result. Enforcing a common order can be difficult, however, because we may not be able to predict, when an operation starts, which data it will eventually need to access. Worse, the fact that correctness depends on locking order means that lock-based program fragments do not compose: we cannot take existing lock-based abstractions and safely call them from within a new critical section.

这些问题表明锁可能是一种太低级的机制。从语义的角度来看,锁和关键部分之间的映射是一个实现细节;我们真正想要的是一个可组合的原子构造。事务内存 (TM) 就是为了提供这一点而尝试的。

These issues suggest that locks may be too low level a mechanism. From a semantic point of view, the mapping between locks and critical sections is an implementation detail; all we really want is a composable atomic construct. Transactional memory (TM) is an attempt to provide exactly that.

无锁的原子性

Atomicity without Locks

长期以来,事务一直被用来实现数据库操作的原子性,并取得了巨大的成功。通常的实现是推测性的:不同线程中的事务同时进行,除非它们在访问数据库中的某些公共记录时发生冲突。在没有冲突的情况下,事务可以完美地并行运行。当发生冲突时,底层系统会在冲突的线程之间进行仲裁。一个线程继续,并希望将其更新提交给数据库;其他线程中止并重新开始(在“回滚”它们迄今为止所做的工作之后)。总体效果是,事务在实现级别实现了显著的并行性,但在程序语义级别似乎以某种全局全序序列化。

Transactions have long been used, with great success, to achieve atomicity for database operations. The usual implementation is speculative: transactions in different threads proceed concurrently unless and until they conflict for access to some common record in the database. In the absence of conflicts, transactions run perfectly in parallel. When conflicts arise, the underlying system arbitrates between the conflicting threads. One gets to continue, and hopefully commit its updates to the database; the others abort and start over (after “rolling back” the work they had done so far). The overall effect is that transactions achieve significant parallelism at the implementation level, but appear to serialize in some global total order at the level of program semantics.

使用更轻量级的事务来实现内存数据结构操作的原子性的想法可以追溯到 1993 年,当时 Herlihy 和 Moss 提出了一个本质上是load_linked/store_conditional指令的多字泛化,如示例 13.29中所述。大约十年后,他们的事务内存(TM) 开始重新受到关注(和更高级别的语义),当时许多研究人员清楚地认识到,只有开发出更简单的编程技术,多核处理器才能成功。

The idea of using more lightweight transactions to achieve atomicity for operations on in-memory data structures dates from 1993, when Herlihy and Moss proposed what was essentially a multiword generalization of the load_linked/ store_conditional, instructions mentioned in Example 13.29. Their transactional memory (TM) began to receive renewed attention (and higher-level semantics) about a decade later, when it became clear to many researchers that multicore processors were going to be successful only with the development of simpler programming techniques.

例 13.47

Example 13.47

一个简单的原子

A simple atomic block

TM 的基本思想非常简单:程序员将代码块标记为原子——

The basic idea of TM is very simple: the programmer labels code blocks as atomic—

原子 {

atomic {

 -— 此处为您的代码

 -— your code here

}

}

— 底层系统负责尽可能并行执行这些块。如果原子块内的代码在发生冲突时可以安全地回滚,那么实现就可以基于推测。■

—and the underlying system takes responsibility for executing these blocks in parallel whenever possible. If the code inside the atomic block can safely be rolled back in the event of conflict, then the implementation can be based on speculation. ■

例 13.48

Example 13.48

有交易的有限缓冲区

Bounded buffer with transactions

在一些基于推测的系统中,需要等待某些先决条件的事务可以“故意”使用显式重试原语中止自身。系统将避免重新启动事务,直到某个先前读取的位置已被另一个线程更改。有界缓冲区的事务代码与图 13.18非常相似。我们只需替换

In some speculation-based systems, a transaction that needs to wait for some precondition can “deliberately” abort itself with an explicit retry primitive. The system will refrain from restarting the transaction until some previously read location has been changed by another thread. Transactional code for a bounded buffer would be very similar to that of Figure 13.18. We would simply replace

当 full_slots < SIZE 时,区域缓冲区当 full_slots > 0 时的区域缓冲区
    

t0015

with

原子原子
  如果 full_slots = SIZE 则重试  如果 full_slots = 0 则重试
    

t0020

TM 避免了指定要实现互斥的对象的需求。它还允许将条件测试放置在原子块内的任何位置。■

TM avoids the need to specify object(s) on which to implement mutual exclusion. It also allows the condition test to be placed anywhere inside the atomic block. ■

人们提出了许多不同的 TM 实现,既有硬件实现,也有软件实现。截至 2015 年,IBM 的 z 和 p 系列机器以及英特尔的 x86 最新版本都已提供硬件支持。Haskell 和几种实验性语言和方言均提供语言级支持。C++ 事务扩展的正式提案正在考虑中,预计将于 2017 年 [ Int15 ] 在该语言的下一个修订版中发布。我们还需要几年时间才能知道 TM 在实践中能将并发简化到何种程度,但目前的迹象令人鼓舞。

Many different implementations of TM have been proposed, both in hardware and in software. As of 2015, hardware support is commercially available in IBM's z and p series machines, and in Intel's recent versions of the x86. Language-level support is available in Haskell and in several experimental languages and dialects. A formal proposal for transactional extensions to C++ is under consideration for the next revision of the language, expected in 2017 [Int15]. It will be several years before we know how much TM can simplify concurrency in practice, but current signs are promising.

示例实现

An Example Implementation

软件 TM 系统种类繁多,令人吃惊。我们在此概述了一种可能的实现,主要基于 Dice 等人的 TL2 系统 [ DSS06 ] 和 Riegel 等人的 TinySTM 系统 [ FRF08 ]。

There is a surprising amount of variety among software TM systems. We outline one possible implementation here, based, in large part, on the TL2 system of Dice et al. [DSS06] and the TinySTM system of Riegel et al. [FRF08].

每个活动事务都会跟踪它已读取的位置以及它已写入的位置和值。它还维护一个valid_time值,该值指示迄今为止它已读取的所有值已知正确的最新逻辑时间。时间是从全局时钟变量中获取的,每次事务尝试提交时,该变量都会加一。最后,线程共享一个全局所有权记录表(orecs),通过对共享位置的地址进行哈希处理来索引。每个 orec 都包含 (1) 该 orec 所涵盖的(哈希到的)任何位置被更新的最近逻辑时间,或 (2) 当前正尝试向其中一个位置提交更改的事务的 ID t。在情况 (1) 中, orec 被称为无主的;在情况 (2) 中, orec (以及哈希到它的所有位置)被称为由t拥有的

Every active transaction keeps track of the locations it has read and the locations and values it has written. It also maintains a valid_time value that indicates the most recent logical time at which all of the values it has read so far were known to be correct. Times are obtained from a global clock variable that increases by one each time a transaction attempts to commit. Finally, threads share a global table of ownership records (orecs), indexed by hashing the address of a shared location. Each orec contains either (1) the most recent logical time at which any of the locations covered by (hashing to) that orec was updated, or (2) the ID t of a transaction that is currently trying to commit a change to one of those locations. In case (1), the orec is said to be unowned; in case (2) the orec—and, by extension, all locations that hash to it—is said to be owned by t.

例 13.49

Example 13.49

原子块的翻译

Translation of an atomic block

编译器将每个原子块翻译成大致相当于下面的代码:

The compiler translates each atomic block into code roughly equivalent to the following:

环形

loop

 有效时间:=时钟

 valid_time := clock

 读取集:=写入映射:=∅

 read_set := write_map := ∅

 尝试

 try

  –– 您的代码在这里

  –– your code here

  犯罪()

  commit()

  休息

  break

 除非中止

 except when abort

  –– 继续循环

  –– continue loop

在事务主体(此处为您的代码)中,对地址为x的位置的读取和写入被替换为对read(x)write(x, v)的调用,使用图 13.19中所示的代码。还显示了提交例程,在上面的try块末尾调用。■

In the body of the transaction (your code here), reads and writes of a location with address x are replaced with calls to read(x) and write(x, v), using the code shown in Figure 13.19. Also shown is the commit routine, called at the end of the try block above. ■

f13-19-9780124104099
图 13.19 软件 TM 系统的可能伪代码读写例程用于替换事务主体内的普通加载和存储。验证例程从读取提交中调用。它尝试验证之前读取的值是否已被覆盖,如果成功,则更新valid_time 如果底层硬件不是顺序一致的,则可能需要各种隔离指令(未显示)。

简而言之,事务会缓冲其(推测性)写入,直到准备好提交为止。然后,它会锁定需要写入的所有位置,验证之前读取的所有位置此后均未被覆盖,然后写回并解锁这些位置。在任何时候,事务都知道其所有读取在有效时间都是相互一致的。如果它尝试读取自有效时间以来已更新的新位置,它会尝试将此时间延长至全局时钟的当前值。如果它能够在提交时执行类似的延长,在锁定需要更改的所有位置之后,那么整个事务的总体效果将就像在提交时立即发生一样。

Briefly, a transaction buffers its (speculative) writes until it is ready to commit. It then locks all the locations it needs to write, verifies that all the locations it previously read have not been overwritten since, and then writes back and unlocks the locations. At all times, the transaction knows that all of its reads were mutually consistent at time valid_time. If it ever tries to read a new location that has been updated since valid_time, it attempts to extend this time to the current value of the global clock. If it is able to perform a similar extension at commit time, after having locked all locations it needs to change, then the aggregate effect of the transaction as a whole will be as if it had occurred instantaneously at commit time.

为了实现重试(图 13.19中未显示),我们可以向每个 orec 添加一个可选的线程列表。重试线程会将自己添加到每个位置的列表中在其read_set中,然后对特定于线程的信号量执行P操作。同时,任何对具有等待线程的 orec 提交更改的线程都会对每个线程的信号量执行V。此机制有时会导致不必要的唤醒,但不会影响正确性。唤醒后,线程会将自己从所有线程列表中删除,然后再重新启动其事务。

To implement retry (not shown in Figure 13.19), we can add an optional list of threads to every orec. A retrying thread will add itself to the list of every location in its read_set and then perform a P operation on a thread-specific semaphore. Meanwhile, any thread that commits a change to an orec with waiting threads performs a V on the semaphore of each of those threads. This mechanism will sometimes result in unnecessary wakeups, but these do not impact correctness. Upon wakeup, a thread removes itself from all thread lists before restarting its transaction.

挑战

Challenges

在我们的示例实现中,许多细节都被掩盖了。如果原子块内的代码抛出异常(除abort之外)或执行返回退出某个周围循环,则示例 13.49中的翻译将无法正常运行。图 13.19的伪代码也没有考虑到事务可能嵌套。

Many subtleties have been glossed over in our example implementation. The translation in Example 13.49 will not behave correctly if code inside the atomic block throws an exception (other than abort) or executes a return or an exit out of some surrounding loop. The pseudocode of Figure 13.19 also fails to consider that transactions may be nested.

TM 设计人员仍在争论其他几个问题。我们应该如何处理事务(I/O、系统调用等)中那些不容易回滚的操作,以及如何防止此类事务调用retry?我们如何阻止程序员创建过大的事务,以至于它们几乎总是相互冲突,并且无法并行运行?程序是否应该能够检测到事务正在中止?事务应如何与锁和非阻塞数据结构交互?事务和非事务代码之间的竞争是否应被视为程序错误?如果是这样,是否应该对可能导致的行为进行任何限制?任何具有生产质量 TM 功能的语言都需要回答这些问题和类似的问题。

Several additional issues are still the subject of debate among TM designers. What should we do about operations inside transactions (I/O, system calls, etc.) that cannot easily be rolled back, and how do we prevent such transactions from ever calling retry? How do we discourage programmers from creating transactions so large they almost always conflict with one another, and cannot run in parallel? Should a program ever be able to detect that transactions are aborting? How should transactions interact with locks and with nonblocking data structures? Should races between transactions and nontransactional code be considered program bugs? If so, should there be any constraints on the behavior that may result? These and similar questions will need to be answered by any production-quality TM-capable language.

13.4.5 隐式同步

13.4.5 Implicit Synchronization

在几种共享内存语言中,线程可以对共享数据执行的操作受到限制,使得同步可以隐含在操作本身中,而不是作为单独的显式操作出现。我们已经看到了一个隐式同步的示例:HPF和 Fortran 95 的forall循环(示例 13.10)。forall循环的单独迭代同时进行,在语义上彼此同步:每次迭代都会读取其第一个赋值语句实例中使用的所有数据,然后才会更新其左侧的实例。左侧的更新依次发生在任何迭代读取其第二个赋值语句实例中使用的数据之前,依此类推。为向量机编译forall循环虽然并非易事,但或多或​​少是直截了当的。然而,在更传统的多处理器上,良好的性能通常取决于高质量的依赖性分析,这使得编译器能够识别循环中的语句实际上不相互依赖并且可以无需同步继续进行的情况。

In several shared-memory languages, the operations that threads can perform on shared data are restricted in such a way that synchronization can be implicit in the operations themselves, rather than appearing as separate, explicit operations. We have seen one example of implicit synchronization already: the forall loop of HPF and Fortran 95 (Example 13.10). Separate iterations of a forall loop proceed concurrently, semantically in lock-step with each other: each iteration reads all data used in its instance of the first assignment statement before any iteration updates its instance of the left-hand side. The left-hand side updates in turn occur before any iteration reads the data used in its instance of the second assignment statement, and so on. Compilation of forall loops for vector machines, while far from trivial, is more or less straightforward. On a more conventional multiprocessor, however, good performance usually depends on high-quality dependence analysis, which allows the compiler to identify situations in which statements within a loop do not in fact depend on one another, and can proceed without synchronization.

依赖性分析在其他语言中也起着至关重要的作用。在侧栏 11.1 中,我们提到了纯函数式语言 Sisal 和 pH(回想一下这些语言中的迭代结构是尾递归的语法糖)。因为这些语言没有副作用,所以它们的结构可以按任何顺序或并发进行评估,只要没有结构尝试使用尚未计算的值。劳伦斯利弗莫尔国家实验室开发的 Sisal 实现使用了大量编译器分析来确定有希望并行执行的结构。它还在数据对象上使用标签来指示对象的值是否已计算。当编译器无法保证在运行时需要某个值时已经计算出来时,生成的代码会使用标签位进行同步、旋转或阻塞,直到它们被正确设置。Sisal 的开发人员在 1992 年声称他们的语言和编译器在性能上可与并行 Fortran 相媲美 [ Can92 ]。

Dependence analysis plays a crucial role in other languages as well. In Sidebar 11.1 we mentioned the purely functional languages Sisal and pH (recall that iterative constructs in these languages are syntactic sugar for tail recursion). Because these languages are side-effect free, their constructs can be evaluated in any order, or concurrently, as long as no construct attempts to use a value that has yet to be computed. The Sisal implementation developed at Lawrence LivermoreNational Lab used extensive compiler analysis to identify promising constructs for parallel execution. It also employed tags on data objects that indicate whether the object's value had been computed yet. When the compiler was unable to guarantee that a value would have been computed by the time it was needed at run time, the generated code used tag bits for synchronization, spinning or blocking until they were properly set. Sisal's developers claimed (in 1992) that their language and compiler rivaled parallel Fortran in performance [Can92].

自动并行化是 20 世纪 80 年代和 90 年代的主要研究课题,最初用于矢量机,后来用于通用机器。它在结构良好的数据并行程序中取得了相当大的成功,主要用于科学应用,并且主要但并非全部用于 Fortran。然而,在更通用、结构不规则的程序中自动识别线程级并行性却很难,消息传递硬件的编译也是如此。该领域的研究仍在继续,并已扩展到 Matlab 和 R 等语言。

Automatic parallelization, first for vector machines and then for general-purpose machines, was a major topic of research in the 1980s and 1990s. It achieved considerable success with well-structured data-parallel programs, largely for scientific applications, and largely but not entirely in Fortran. Automatic identification of thread-level parallelism in more general, irregularly structured programs proved elusive, however, as did compilation for message-passing hardware. Research in this area continues, and has branched out to languages like Matlab and R.

期货

Futures

例 13.50

Example 13.50

Multilisp 中的未来构造

future construct in Multilisp

隐式同步也可以在不进行编译器分析的情况下实现。Scheme 的 Multilisp [ Hal85 , MKH91 ] 方言允许程序员将任何函数求值封装在特殊的Future构造中:

Implicit synchronization can also be achieved without compiler analysis. The Multilisp [Hal85, MKH91] dialect of Scheme allowed the programmer to enclose any function evaluation in a special future construct:

(未来(我的函数我的参数))

(future (my-function my-args))

在纯函数式程序中,future在语义上是中立的:假设所有求值都终止,程序行为将与( my-function my-args )在没有周围调用的情况下出现时完全相同。然而,在实现中,future安排嵌入函数由单独的控制线程进行求值。父线程继续执行,直到它实际尝试使用my–function的返回值,此时它等待Future的执行完成。如果函数的两个或多个参数包含在futures中,则参数的求值可以并行进行:

In a purely functional program, future is semantically neutral: assuming all evaluations terminate, program behavior will be exactly the same as if (my-function my-args) had appeared without the surrounding call. In the implementation, however, future arranges for the embedded function to be evaluated by a separate thread of control. The parent thread continues to execute until it actually tries to use the return value of my–function, at which point it waits for execution of the future to complete. If two or more arguments to a function are enclosed in futures, then evaluation of the arguments can proceed in parallel:

(父级(未来(子级 1参数 1))(未来(子级 2参数 2)))

(parent (future (child1 args1)) (future (child2 args2)))

例 13.51

Example 13.51

C# 中的 Future

Futures in C#

Multilisp 中没有额外的同步机制:future本身是该语言对 Scheme 的唯一补充。许多后续语言和系统都提供了Future作为更大功能集的一部分。使用 C# 的任务并行库 (TPL),我们可以写

There were no additional synchronization mechanisms in Multilisp: future itself was the language's only addition to Scheme. Many subsequent languages and systems have provided future as part of a larger feature set. Using C#'s Task Parallel Library (TPL), we might write

var 描述 = Task.Factory.StartNew(() => GetDescription());

var description = Task.Factory.StartNew(() => GetDescription());

var numberInStock = Task.Factory.StartNew(() => GetInventory());

var numberInStock = Task.Factory.StartNew(() => GetInventory());

Console.WriteLine(“我们有” + numberInStock.Result + “份” + description.Result + “有货”);

Console.WriteLine(“We have” + numberInStock.Result + “copies of” + description.Result + “in stock”);

静态库类Task.Factory用于生成 Future,在 C# 中称为“任务”。Create方法支持泛型类型推断,允许我们为任何T传递与Func<T>(返回T 的函数)兼容的委托。我们在此处将委托指定为 lambda 表达式。如果GetDescription返回字符串,则 description 的类型为Task<String>;如果GetInventory返回int,则numberInStock的类型为Task<int>

Static library class Task.Factory is used to generate futures, known as “tasks” in C#. The Create method supports generic type inference, allowing us to pass a delegate compatible with Func<T> (function returning T), for any T. We've specified the delegates here as lambda expressions. If GetDescription returns a String, description will be of type Task<String>; if GetInventory returns an int, numberInStock will be of type Task<int>.

Java 标准库提供了类似的功能,但是由于缺少委托、属性(如Result)、类型推断(var)和自动装箱(GetInventory返回的int),语法变得相当繁琐。Java 还要求程序员将新创建的Future传递给明确创建的Executor对象,该对象将负责运行它们。Scala 为 Future 提供的语法与 C# 一样简单,但语义更丰富。■

The Java standard library provides similar facilities, but the lack of delegates, properties (like Result), type inference (var), and automatic boxing (of the int returned by GetInventory) make the syntax quite a bit more cumbersome. Java also requires that the programmer pass newly created Futures to an explicitly created Executor object that will be responsible for running them. Scala provides syntax for futures as simple as that of C#, with even richer semantics. ■

例 13.52

Example 13.52

C++11 中的 Future

Futures in C++11

Future 也可用于 C++,其设计目的是与 lambda 表达式、对象闭包以及各种线程和异步(延迟)计算机制进行互操作。也许最简单的用例是使用通用async函数,它接受一个函数f和一个参数列表a 1 , …, a n ,并返回一个最终将产生f(a 1 , …, a n ) 的Future :

Futures are also available in C++, where they are designed to interoperate with lambda expressions, object closures, and a variety of mechanisms for threading and asynchronous (delayed) computation. Perhaps the simplest use case employs the generic async function, which takes a function f and a list of arguments a1, …, an, and returns a future that will eventually yield f(a1, …, an):

字符串 ip_address_of(const char* 主机名) {

string ip_address_of(const char* hostname) {

 // 进行互联网名称查找(可能很慢)

 // do Internet name lookup (potentially slow)

}

}

设计与实现

Design & Implementation

13.9 无副作用和隐式同步

13.9 Side-effect freedom and implicit synchronization

在部分命令式程序(Multilisp、C#、Scala 等)中,程序员必须小心确保Future的并发执行不会损害程序的正确性。如果child1child2的求值相互依赖,或者如果 parent 的求值依赖于child1child2的任何方面(而不是它们的返回值),则表达式 ( parent (future (child1 args1 )) (future (child2 args2 )))可能会产生不可预测的行为。这种行为可能很难调试。Sisal 和 Haskell 等语言通过仅允许无副作用的程序来避免此问题。

In a partially imperative program (in Multilisp, C#, Scala, etc.), the programmer must take care to make sure that concurrent execution of futures will not compromise program correctness. The expression (parent (future (child1 args1)) (future (child2 args2))) may produce unpredictable behavior if the evaluations of child1 and child2 depend on one another, or if the evaluation of parent depends on any aspect of child1 and child2 other than their return values. Such behavior may be very difficult to debug. Languages like Sisal and Haskell avoid the problem by permitting only side-effect–free programs.

从关键意义上讲,纯函数式语言非常适合并行执行:它们消除了表达式之间的所有人为连接(所有反向和输出依赖关系(第 C-17.6 节)):剩下的只是实际的数据流。性能方面仍存在两个主要障碍:(1) 函数式程序高效代码生成的标准挑战(第 11.8 节),以及 (2) 需要确定哪些潜在并行代码片段足够大且足够独立,值得创建线程和隐式同步。

In a key sense, pure functional languages are ideally suited to parallel execution: they eliminate all artificial connections—all anti- and output dependences (Section C-17.6)—among expressions: all that remains is the actual data flow. Two principal barriers to performance remain: (1) the standard challenges of efficient code generation for functional programs (Section 11.8), and (2) the need to identify which potentially parallel code fragments are large enough and independent enough to merit the overhead of thread creation and implicit synchronization.

自动查询 = 异步(ip_address_of,“ www.cs.rochester.edu ”);

auto query = async(ip_address_of, “www.cs.rochester.edu“);

cout << query.get() << “\n”; // 打印“192.5.53.208”

cout << query.get() << “\n”;    // prints “192.5.53.208”

这里我们用auto关键字声明的变量query将被推断为具有future<string>类型。■

Here variable query, which we declared with the auto keyword, will be inferred to have type future<string>. ■

在某些方面, Multilisp 的Future构造类似于Scheme 的内置delayforce (第 6.6.2 节)。然而,Future支持并发,而Delay支持惰性求值:它推迟对其嵌入函数的求值,直到知道需要返回值为止。在 Scheme 中,任何使用Delay表达式都必须用Force包围。相比之下,Multilisp Future上的同步是隐式的 — 没有Force的类似物。

In some ways the future construct of Multilisp resembles the built-in delay and force of Scheme (Section 6.6.2). Where future supports concurrency, however, delay supports lazy evaluation: it defers evaluation of its embedded function until the return value is known to be needed. Any use of a delayed expression in Scheme must be surrounded by force. By contrast, synchronization on a Multilisp future is implicit—there is no analog of force.

C++ async的一个更复杂的变体(示例 13.52中未使用)允许程序员坚持在单独的线程中运行未来,或者,在调用get之前保持未求值状态(此时它将在调用线程中执行)。当使用async时(如我们的示例所示),实现的选择留给运行时系统(就像在 Multilisp 中一样)。

A more complicated variant of the C++ async, not used in Example 13.52, allows the programmer to insist that the future be run in a separate thread—or, alternatively, that it remain unevaluated until get is called (at which point it will execute in the calling thread). When async is used as shown in our example, the choice of implementation is left to the run-time system—as it is in Multilisp.

并行逻辑编程

Parallel Logic Programming

一些研究人员已经注意到,诸如 Prolog 之类的逻辑语言的回溯搜索也适合并行化。有两种可能的策略。第一种是并行追求规则右侧的子目标。这种策略称为AND 并行。逻辑中的变量一旦初始化就不会再被修改,这一事实确保了AND的并行分支不会互相干扰。第二种策略称为OR 并行;它并行追求替代解决方案。由于它们通常采用不同的统一,因此OR的分支必须使用其变量的单独副本。在如图 12.1所示的搜索树中,AND并行和OR并行在交替的级别创建新的线程。

Several researchers have noted that the backtracking search of logic languages such as Prolog is also amenable to parallelization. Two strategies are possible. The first is to pursue in parallel the subgoals found in the right-hand side of a rule. This strategy is known as AND parallelism. The fact that variables in logic, once initialized, are never subsequently modified ensures that parallel branches of an AND cannot interfere with one another. The second strategy is known as OR parallelism; it pursues alternative resolutions in parallel. Because they will generally employ different unifications, branches of an OR must use separate copies of their variables. In a search tree such as that of Figure 12.1, AND parallelism and OR parallelism create new threads at alternating levels.

OR并行是推测性的:由于只需要在一个分支上成功,因此在其他分支上进行的工作在某种意义上是浪费。但是,当无法满足目标(在这种情况下必须搜索整个树)或以不同方式满足目标所需的执行时间差异很大(在这种情况下同时探索多个分支会减少找到第一个解决方案的预期时间)时,OR 并行会很好地工作。在 Prolog 中, ANDOR并行都是有问题的,因为它们未能遵循语言语义所要求的确定性搜索顺序。Parlog [ Che92 ] 同时支持ANDOR并行,是最著名的并行 Prolog 方言。

OR parallelism is speculative: since success is required on only one branch, work performed on other branches is in some sense wasted. OR parallelism works well, however, when a goal cannot be satisfied (in which case the entire tree must be searched), or when there is high variance in the amount of execution time required to satisfy a goal in different ways (in which case exploring several branches at once reduces the expected time to find the first solution). Both AND and OR parallelism are problematic in Prolog, because they fail to adhere to the deterministic search order required by language semantics. Parlog [Che92], which supports both AND and OR parallelism, is the best known of the parallel Prolog dialects.

13-01-9780124104099检查你的理解

Check Your Understanding

38. 什么是监视器?监视器条件变量与信号量有何不同?

38. What is a monitor?. How do monitor condition variables differ from semaphores?

39.解释将监控信号视为 提示和将其视为绝对值之间的区别。

39. Explain the difference between treating monitor signals as hints and treating them as absolutes.

40. 什么是监视不变量?在什么情况下必须保证它成立?

40. What is a monitor invariant? Under what circumstances must it be guaranteed to hold?

41. 描述嵌套监控问题及一些潜在的解决方案。

41. Describe the nested monitor problem and some potential solutions.

42. 什么是死锁

42. What is deadlock?

43. 什么是条件临界区?它和监视器有什么不同?

43. What is a conditional critical region? How does it differ from a monitor?

44. 总结 Ada 95、Java 和 C# 的同步机制。将它们相互对比,并与监视器和条件临界区进行对比。务必解释 Java 5 中新增的功能。

44. Summarize the synchronization mechanisms of Ada 95, Java, and C#. Contrast them with one another, and with monitors and conditional critical regions. Be sure to explain the features added to Java 5.

45. 什么是事务内存?与基于锁的算法相比,它有哪些优势?在广泛使用之前需要克服哪些挑战?

45. What is transactional memory? What advantages does it offer over algorithms based on locks? What challenges will need to be overcome before it enters widespread use?

46.描述 HPF/Fortran 95  forall循环的语义。它与do parallel有何不同?

46. Describe the semantics of the HPF/Fortran 95 forall loop. How does it differ from do concurrent?

47. 为什么说纯函数式语言为并发编程提供了特别有吸引力的环境?

47. Why might pure functional languages be said to provide a particularly attractive setting for concurrent programming?

48. 什么是Future?它们出现在哪些语言中?程序员在使用它们时必须采取哪些预防措施?

48. What are futures? In what languages do they appear? What precautions must the programmer take when using them?

49.解释 Prolog中AND并行OR并行的区别。

49. Explain the difference between AND parallelism and OR parallelism in Prolog.

13.5 消息传递

13.5 Message Passing

共享内存并发在多核处理器和多处理器服务器上已变得无处不在。然而,消息传递仍然主导着分布式和高端计算。超级计算机和大型集群主要使用 Fortran 或 C/C++ 以及 MPI 库包进行编程。分布式计算越来越依赖于基于实现 TCP/IP 互联网标准的库的客户端-服务器抽象层。与共享内存计算一样,还开发了数十种消息传递语言用于特定应用领域,或用于研究或教学目的。

Shared-memory concurrency has become ubiquitous on multicore processors and multiprocessor servers. Message passing, however, still dominates both distributed and high-end computing. Supercomputers and large-scale clusters are programmed primarily in Fortran or C/C++ with the MPI library package. Distributed computing increasingly relies on client–server abstractions layered on top of libraries that implement the TCP/IP Internet standard. As in shared-memory computing, scores of message-passing languages have also been developed for particular application domains, or for research or pedagogical purposes.

13-02-9780124104099 更深入地

IN MORE DEPTH

配套站点探讨了基于消息的并发中的三个核心问题——命名、发送和接收。名称可以直接引用进程、与进程关联的某些通信资源(通常称为条目端口,或独立的套接字通道抽象发送操作可能完全是异步的,在这种情况下,发送方在底层系统尝试传递消息时继续,或者发送方可能等待,通常是等待确认收据或返回答复。接收操作可以显式执行,也可以隐式触发某些先前指定的处理程序例程的执行。当隐式接收与等待答复的发送方相结合时,这种组合通常称为远程过程调用(RPC)。除了消息传递库之外,RPC 系统通常还依赖于一种称为存根编译器的语言感知工具。

Three central issues in message-based concurrency—naming, sending, and receiving—are explored on the companion site. A name may refer directly to a process, to some communication resource associated with a process (often called an entry or port), or to an independent socket or channel abstraction. A send operation may be entirely asynchronous, in which case the sender continues while the underlying system attempts to deliver the message, or the sender may wait, typically for acknowledgment of receipt or for the return of a reply. A receive operation, for its part, may be executed explicitly, or it may implicitly trigger execution of some previously specified handler routine. When implicit receipt is coupled with senders waiting for replies, the combination is typically known as remote procedure call (RPC). In addition to message-passing libraries, RPC systems typically rely on a language-aware tool known as a stub compiler.

13.6 总结和结束语

13.6 Summary and Concluding Remarks

并发和并行在现代计算机系统中无处不在。可以肯定地说,当今大多数计算机研究和开发都以某种形式涉及并发。高端计算机系统一直都是并行的,多核 PC 和手机现在无处不在。即使在单处理器上,图形和网络应用程序通常也是并发的。

Concurrency and parallelism have become ubiquitous in modern computer systems. It is probably safe to say that most computer research and development today involves concurrency in one form or another. High-end computer systems have always been parallel, and multicore PCs and cellphones are now ubiquitous. Even on uniprocessors, graphical and networked applications are typically concurrent.

本章介绍了并发编程,重点介绍了编程语言问题。我们首先概述了并发的动机和现代多处理器的体系结构。然后,我们概述了并发软件的基础知识,包括通信、同步以及线程的创建和管理。我们区分了共享内存和消息传递通信和同步模型,以及基于语言和基于库的并发实现。

In this chapter we have provided an introduction to concurrent programming with an emphasis on programming language issues. We began with an overview of the motivations for concurrency and of the architecture of modern multiprocessors. We then surveyed the fundamentals of concurrent software, including communication, synchronization, and the creation and management of threads. We distinguished between shared-memory and message-passing models of communication and synchronization, and between language- and library-based implementations of concurrency.

我们对线程创建和管理的调查描述了创建线程的六种不同构造:co-begin、并行循环、launch-at-elaboration、fork/join、隐式接收和早期回复。其中fork/join是最常见的;它存在于许多语言中,以及基于库的软件包中,例如 MPI 和 OpenMP。RPC 系统通常在内部使用fork/join来实现隐式接收。无论线程创建机制如何,大多数并发编程系统都在操作系统级进程集合之上实现其语言级或库级线程,操作系统以类似的方式在硬件核心集合之上实现这些进程。我们分阶段构建了示例实现,从单处理器上的协程开始,然后添加就绪列表和调度程序,然后添加用于抢占的计时器,最后在多个核心上进行并行调度。

Our survey of thread creation and management described some six different constructs for creating threads: co-begin, parallel loops, launch-at-elaboration, fork/join, implicit receipt, and early reply. Of these fork/join is the most common; it is found in a host of languages, and in library-based packages such as MPI and OpenMP. RPC systems typically use fork/join internally to implement implicit receipt. Regardless of the thread-creation mechanism, most concurrent programming systems implement their language- or library-level threads on top of a collection of OS-level processes, which the operating system implements in a similar manner on top of a collection of hardware cores. We built our sample implementation in stages, beginning with coroutines on a uniprocessor, then adding a ready list and scheduler, then timers for preemption, and finally parallel scheduling on multiple cores.

本章的大部分内容集中在共享内存编程模型上,尤其是同步。我们区分了原子性和条件同步,以及忙等待和基于调度程序的实现。在忙等待机制中,我们特别研究了自旋锁和屏障。在基于调度程序的机制中,我们研究了信号量、监视器和条件临界区。在这三种机制中,信号量是最简单的,并且仍然被广泛使用,特别是在操作系统中。监视器和条件临界区提供了更好的封装和抽象,但不适合在库中实现。条件临界区可能被认为提供了最令人愉快的编程模型,但通常不能像监视器那样有效地实现。

The bulk of the chapter focused on shared-memory programming models, and on synchronization in particular. We distinguished between atomicity and condition synchronization, and between busy-wait and scheduler-based implementations. Among busy-wait mechanisms we looked in particular at spin locks and barriers. Among scheduler-based mechanisms we looked at semaphores, monitors, and conditional critical regions. Of the three, semaphores are the simplest, and remain widely used, particularly in operating systems. Monitors and conditional critical regions provide better encapsulation and abstraction, but are not amenable to implementation in a library. Conditional critical regions might be argued to provide the most pleasant programming model, but cannot in general be implemented as efficiently as monitors.

我们还考虑了并行函数式语言和高性能 Fortran 等数据并行语言的并行化编译器提供的隐式同步。对于以函数式风格编写的程序,我们考虑了Multilisp 引入的未来机制,该机制随后被纳入许多其他语言,包括 Java、C#、C++ 和 Scala。

We also considered the implicit synchronization provided by parallel functional languages and by parallelizing compilers for such data-parallel languages as High Performance Fortran. For programs written in a functional style, we considered the future mechanism introduced by Multilisp and subsequently incorporated into many other languages, including Java, C#, C++, and Scala.

作为基于锁的原子性的替代方案,我们考虑了非阻塞数据结构,它可以避免由于不合时宜的抢占和页面错误而导致的性能异常。对于某些常见结构,即使在常见情况下,非阻塞算法也可以胜过锁。不幸的是,它们往往非常微妙且难以创建。

As an alternative to lock-based atomicity, we considered nonblocking data structures, which avoid performance anomalies due to inopportune preemption and page faults. For certain common structures, nonblocking algorithms can outperform locks even in the common case. Unfortunately, they tend to be extraordinarily subtle and difficult to create.

事务内存 (TM) 最初被认为是一种为任意数据结构构建非阻塞代码的通用方法。然而,最近的大多数实现都放弃了非阻塞保证,而是专注于指定原子性而无需设计显式锁定协议的能力。与条件临界区一样,TM 牺牲了性能来换取可编程性。现在,各种语言都有原型实现,并且有多种商业指令集的硬件支持。

Transactional memory (TM) was originally conceived as a general-purpose means of building nonblocking code for arbitrary data structures. Most recent implementations, however, have given up on nonblocking guarantees, focusing instead on the ability to specify atomicity without devising an explicit locking protocol. Like conditional critical regions, TM sacrifices performance for the sake of programmability. Prototype implementations are now available for a wide variety of languages, with hardware support in several commercial instruction sets.

我们关于消息传递的部分(主要在配套网站上)从多个库和语言中选取了示例,并考虑了进程如何命名彼此、发送消息时阻塞多长时间以及接收是隐式的还是显式的。分布式计算越来越依赖于远程过程调用,它将远程调用发送(等待回复)与隐式消息接收相结合。

Our section on message passing, mostly on the companion site, drew examples from several libraries and languages, and considered how processes name each other, how long they block when sending a message, and whether receipt is implicit or explicit. Distributed computing increasingly relies on remote procedure calls, which combine remote-invocation send (wait for a reply) with implicit message receipt.

与前面的章节一样,我们看到了许多语言设计和语言实现相互影响的情况。一些机制(仙人掌堆栈、条件临界区、基于内容的消息筛选)非常复杂,许多语言设计者选择不提供它们。其他机制(Ada 样式的参数模式)是专门为促进有效的实现技术而开发的。而在其他情况下(无等待发送的语义、监视器内的阻塞),实现问题在更大的权衡中起着重要作用。

As in previous chapters, we saw many cases in which language design and language implementation influence one another. Some mechanisms (cactus stacks, conditional critical regions, content-based message screening) are sufficiently complex that many language designers have chosen not to provide them. Other mechanisms (Ada-style parameter modes) have been developed specifically to facilitate an efficient implementation technique. And in still other cases (the semantics of no-wait send, blocking inside a monitor), implementation issues play a major role in some larger set of tradeoffs.

尽管并发语言设计的历史非常悠久,但直到最近,大多数多线程程序都依赖于基于库的线程包。然而,即使是 C 和 C++ 现在也明确是并行的,很难想象任何新的专为纯顺序执行而设计的语言。截至 2015 年,显式并行语言尚未严重破坏 MPI 在高端科学计算领域的主导地位,尽管这种情况在未来几年也可能会发生变化。

Despite the very long history of concurrent language design, until recently most multithreaded programs relied on library-based thread packages. Even C and C++ are now explicitly parallel, however, and it is hard to imagine any new languages being designed for purely sequential execution. As of 2015, explicitly parallel languages have yet to seriously undermine the dominance of MPI for high-end scientific computing, though this, too, may change in coming years.

13.7 练习

13.7 Exercises

13.1 给出一个“良性”竞争条件的例子——其结果会影响程序行为,但不会影响正确性。

13.1 Give an example of a “benign” race condition—one whose outcome affects program behavior, but not correctness.

13.2 我们已经定义了线程包的就绪列表,以包含所有可运行但未运行的线程,并使用单独的变量来标识当前正在运行的线程。我们是否可以同样轻松地定义就绪列表以包含所有可运行的线程,并理解列表开头的线程正在运行?(提示:考虑多处理器。)

13.2 We have defined the ready list of a thread package to contain all threads that are runnable but not running, with a separate variable to identify the currently running thread. Could we just as easily have defined the ready list to contain all runnable threads, with the understanding that the one at the head of the list is running? (Hint: Think about multiprocessors.)

13.3 假设你正在编写代码来管理一个将在多个并发线程之间共享的哈希表。假设对该表的操作必须是原子的。你可以使用一个互斥锁来保护整个表,或者你可以设计一个方案,每个哈希表存储桶一个锁。哪种方法可能效果更好,在什么情况下?为什么?

13.3 Imagine you are writing the code to manage a hash table that will be shared among several concurrent threads. Assume that operations on the table need to be atomic. You could use a single mutual exclusion lock to protect the entire table, or you could devise a scheme with one lock per hashtable bucket. Which approach is likely to work better, under what circumstances? Why?

13.4 典型的自旋锁只保存一位数据,但需要存储一个完整的字,因为在硬件中,只有完整的字才能被原子地读取、修改和写入。但是,请考虑上一个练习中的哈希表。如果我们选择对表的每个存储桶使用单独的锁,请解释如何实现“两级”锁定方案,该方案将表作为一个整体的传统自旋锁与每个存储桶的单个锁定信息相结合。解释为什么这种方案可能是可取的,特别是在具有外部链接的表中。

13.4 The typical spin lock holds only one bit of data, but requires a full word of storage, because only full words can be read, modified, and written atomically in hardware. Consider, however, the hash table of the previous exercise. If we choose to employ a separate lock for each bucket of the table, explain how to implement a “two-level” locking scheme that couples a conventional spin lock for the table as a whole with a single bit of locking information for each bucket. Explain why such a scheme might be desirable, particularly in a table with external chaining.

13.5从 示例 13.2913.30中汲取灵感,使用compare_and_swap设计一个堆栈的非阻塞链表实现。(当CAS首次在 IBM 370 架构上推出时,该算法是驱动应用之一 [ Tre86 ]。)

13.5 Drawing inspiration from Examples 13.29 and 13.30, design a nonblocking linked-list implementation of a stack using compare_and_swap. (When CAS was first introduced, on the IBM 370 architecture, this algorithm was one of the driving applications [Tre86].)

13.6 在上一个练习的基础上,假设堆栈节点是动态分配的。如果我们读取一个指针,然后被延迟(例如,由于抢占),则指针指向的节点可能会被回收,然后重新分配用于其他目的。随后的比较和交换可能会成功,而从逻辑上讲它不应该成功。这个问题被称为ABA 问题。

给出一个具体的例子——两个或多个线程中的操作交错——其中 ABA 问题可能导致堆栈行为不正确。解释为什么这种行为不会发生在具有自动垃圾收集的系统中。建议在具有手动存储管理的系统中可以采取哪些措施来避免这种情况。

13.6 Building on the previous exercise, suppose that stack nodes are dynamically allocated. If we read a pointer and then are delayed (e.g., due to preemption), the node to which the pointer refers may be reclaimed and then reallocated for a different purpose. A subsequent compare-and-swap may then succeed when logically it should not. This issue is known as the ABA problem.

Give a concrete example—an interleaving of operations in two or more threads—where the ABA problem may result in incorrect behavior for your stack. Explain why this behavior cannot occur in systems with automatic garbage collection. Suggest what might be done to avoid it in systems with manual storage management.

13.7 我们在第 13.3.2 节中指出 包括 ARM、MIPS 和 Power 在内的多种处理器都提供了compare_and_swap (CAS)的替代方案,称为load_linked /store_conditional (LL/SC)。load_linked指令将内存位置加载到寄存器中,并将某些簿记信息存储到隐藏的处理器寄存器中。store_conditional指令将寄存器存储回内存位置,但前提是自执行load_linked以来该位置未被任何其他处理器修改。与compare_and_swap一样,store_conditional返回是否成功的指示。

13.7 We noted in Section 13.3.2 that several processors, including the ARM, MIPS, and Power, provide an alternative to compare_and_swap (CAS) known as load_linked/store_conditional (LL/SC). A load_linked instruction loads a memory location into a register and stores certain bookkeeping information into hidden processor registers. A store_conditional instruction stores the register back into the memory location, but only if the location has not been modified by any other processor since the load_linked was executed. Like compare_and_swap, store_conditional returns an indication of whether it succeeded or not.

(a) 使用LL/SC重写示例 13.29的代码序列。

(a) Rewrite the code sequence of Example 13.29 using LL/SC.

(b) 在大多数机器上,SC指令可能由于多种“虚假”原因而失败,包括页面错误、缓存未命中或自匹配LL以来发生中断。程序员必须采取哪些步骤来确保算法在遇到此类故障时正常工作?

(b) On most machines, an SC instruction can fail for any of several “spurious” reasons, including a page fault, a cache miss, or the occurrence of an interrupt in the time since the matching LL. What steps must a programmer take to make sure that algorithms work correctly in the face of such failures?

(c)讨论 LL/SCCAS的相对优势。考虑如何在缓存一致性多处理器上实现它们。是否存在一种可以工作而另一种不工作的情况?(提示:考虑线程可能需要接触多个内存位置的算法。还要考虑内存位置的内容可能被更改然后恢复的算法,如上一个练习中所述。)

(c) Discuss the relative advantages of LL/SC and CAS. Consider how they might be implemented on a cache-coherent multiprocessor. Are there situations in which one would work but the other would not? (Hints: Consider algorithms in which a thread may need to touch more than one memory location. Also consider algorithms in which the contents of a memory location might be changed and then restored, as in the previous exercise.)

13.8 图 13.8中的测试和设置锁开始,实现忙等待代码,允许读者并发访问数据结构。写者仍然需要锁定读者和其他写者。您可以使用任何合理的原子指令(例如LL/SC)。考虑公平性问题。特别是,如果总是有读者对访问数据结构感兴趣,您的算法应该确保写者不会被永远锁定。

13.8 Starting with the test-and-test_and_set lock of Figure 13.8, implement busy-wait code that will allow readers to access a data structure concurrently. Writers will still need to lock out both readers and other writers. You may use any reasonable atomic instruction(s) (e.g., LL/SC). Consider the issue of fairness. In particular, if there are always readers interested in accessing the data structure, your algorithm should ensure that writers are not locked out forever.

13.9 假设 Java 内存模型,

13.9 Assuming the Java memory model,

(a)解释为什么在 图 13.11中将XY标记volatile是不够的。

(a) Explain why it is not sufficient in Figure 13.11 to label X and Y as volatile.

(b) 解释为什么在同一个图中,将C的读取(类似地,D的读取)封闭在某些公共共享对象O 的同步块中足够了。

(b) Explain why it is sufficient, in that same figure, to enclose C's reads (and similarly those of D) in a synchronized block for some common shared object O.

(c)解释为什么在 示例 13.31中将inspectedX都标记volatile就足够了,而不能只标记一个。

(c) Explain why it is sufficient, in Example 13.31, to label both inspected and X as volatile, but not to label only one.

(提示:您可能会发现查阅 Doug Lea 的 Java 内存模型“编译器编写者指南”很有用,网址为gee.cs.oswego.edu/dl/jmm/cookbook. html)。

(Hint: You may find it useful to consult Doug Lea's Java Memory Model “Cookbook for Compiler Writers,” at gee.cs.oswego.edu/dl/jmm/cookbook. html).

13.10 在 x86 上实现示例 13.30中的非阻塞队列。(完整的伪代码可以在 Michael 和 Scott 的论文 [ MS98 ] 中找到。)

您是否需要隔离指令来确保一致性?如果您可以使用合适的硬件,请将您的代码移植到具有更宽松内存模型的机器(例如 ARM 或 Power)。您需要哪些新的隔离指令或原子引用?

13.10 Implement the nonblocking queue of Example 13.30 on an x86. (Complete pseudocode can be found in the paper by Michael and Scott [MS98].)

Do you need fence instructions to ensure consistency? If you have access to appropriate hardware, port your code to a machine with a more relaxed memory model (e.g., ARM or Power). What new fences or atomic references do you need?

13.11考虑 图 13.19中的软件事务内存的实现。

13.11 Consider the implementation of software transactional memory in Figure 13.19.

(a) 您将如何实现read_setwrite_maplock_map数据结构?您不仅希望最小化插入和查找操作的成本,还希望最小化 (1) 在事务结束时将表“清零”,以便可以再次使用;以及 (2) 如果表太满,则扩展表。

(a) How would you implement the read_set, write_map, and lock_map data structures? You will want to minimize the cost not only of insert and lookup operations but also of (1) “zeroing out” the table at the end of a transaction, so it can be used again; and (2) extending the table if it becomes too full.

(b) 验证例程在两个不同的地方被调用。内联扩展这些调用并根据调用上下文对其进行自定义。您可以实现哪些优化?

(b) The validate routine is called in two different places. Expand these calls in-line and customize them to the calling context. What optimizations can you achieve?

(c) 优化提交例程,利用这样一个事实:如果自valid_time以来没有其他事务提交,则不需要最终验证。

(c) Optimize the commit routine to exploit the fact that a final validation is unnecessary if no other transaction has committed since valid_time.

(d) 通过观察finally子句中的for循环实际上需要遍历 orecs,而不是遍历地址(如果多个地址散列到同一个 orec,则可能会有差异),进一步优化提交。理想情况下, lock_map应该保存哪些数据?

(d) Further optimize commit by observing that the for loop in the finally clause really needs to iterate over orecs, not over addresses (there may be a difference, if more than one address hashes to the same orec). What data, ideally, should lock_map hold?

13.12 可以公平地指责示例 13.35中的代码抽象性较差。如果我们将desire_condition设为委托(子例程或对象闭包),是否可以将其作为额外参数传递,并将信号和scheduler_lock管理移到sleep_on中?(提示:考虑图 13.15中 P 操作的代码。)

13.12 The code of Example 13.35 could fairly be accused of displaying poor abstraction. If we make desired_condition a delegate (a subroutine or object closure), can we pass it as an extra parameter, and move the signal and scheduler_lock management inside sleep_on? (Hint: Consider the code for the P operation in Figure 13.15.)

13.13 图 13.13中用于使调度程序代码可重入的机制对应用程序的所有调度数据结构使用单个操作系统提供的锁。除其他外,此机制可防止不同处理器上的线程对不相关的信号量执行PV操作,即使这些操作都不需要阻塞。您能否设计出另一种用于调度程序相关操作的同步机制,该机制允许更高程度的并发性,但仍然正确?

13.13 The mechanism used in Figure 13.13 to make scheduler code reentrant employs a single OS-provided lock for all the scheduling data structures of the application. Among other things, this mechanism prevents threads on separate processors from performing P or V operations on unrelated semaphores, even when none of the operations needs to block. Can you devise another synchronization mechanism for scheduler-related operations that admits a higher degree of concurrency but that is still correct?

13.14 说明如何将基于锁的并发集实现为单向链接排序列表。您的实现应支持插入、查找删除操作,并应允许对列表的不同部分进行并发操作(因此对整个列表使用单个锁是不够的)。(提示:您将需要使用“行走锁”习语,其中获取和释放操作以非后进先出顺序交错进行。)

13.14 Show how to implement a lock-based concurrent set as a singly linked sorted list. Your implementation should support insert, find, and remove operations, and should permit operations on separate portions of the list to occur concurrently (so a single lock for the entire list will not suffice). (Hint: You will want to use a “walking lock” idiom in which acquire and release operations are interleaved in non-LIFO order.)

13.15  (困难)实现上一个练习中集合的非阻塞版本。(提示:你可能会发现插入很容易,但删除很难。考虑一种惰性删除机制,其中清理(物理删除节点)可能在删除的逻辑完成后发生。有关详细信息,请参阅 Harris [ Har01 ]的工作。)

13.15 (Difficult) Implement a nonblocking version of the set of the previous exercise. (Hint: You will probably discover that insertion is easy but deletion is hard. Consider a lazy deletion mechanism in which cleanup [physical removal of a node] may occur well after logical completion of the removal. For further details see the work of Harris [Har01].)

13.16 为了使自旋锁在多道程序多处理器上有用,可能需要确保在临界区中间不会有任何进程被抢占。这样,在用户空间中自旋就总是安全的,因为可以保证持有锁的进程在其他处理器上运行,而不是被抢占并可能需要当前处理器。解释为什么操作系统设计者可能不想让用户进程能够任意禁用抢占。(提示:考虑公平性和多用户。)你能建议一种解决这个问题的方法吗?(在 Kontothanassis、Wisniewski 和 Scott 的论文 [ KWS97 ] 中可以找到对几种可能解决方案的引用。)

13.16 To make spin locks useful on a multiprogrammed multiprocessor, one might want to ensure that no process is ever preempted in the middle of a critical section. That way it would always be safe to spin in user space, because the process holding the lock would be guaranteed to be running on some other processor, rather than preempted and possibly in need of the current processor. Explain why an operating system designer might not want to give user processes the ability to disable preemption arbitrarily. (Hint: Think about fairness and multiple users.) Can you suggest a way to get around the problem? (References to several possible solutions can be found in the paper by Kontothanassis, Wisniewski, and Scott [KWS97].)

13.17 说明如何使用信号量构建基于调度程序的n线程屏障。

13.17 Show how to use semaphores to construct a scheduler-based n-thread barrier.

13.18 证明监视器和信号量同样强大。也就是说,用一个来实现另一个。在基于监视器的信号量实现中,你的监视器不变量是什么?

13.18 Prove that monitors and semaphores are equally powerful. That is, use each to implement the other. In the monitor-based implementation of semaphores, what is your monitor invariant?

13.19 说明如何使用二进制信号量实现通用信号量。

13.19 Show how to use binary semaphores to implement general semaphores.

13.20 示例 13.38图 13.15 )中,假设我们将过程 P的中间四行替换为if SN = 0 sleep_on(SQ) SN -:= 1,将过程 V 的中间四行替换为S.N +:= 1 if SQ is nonempty enqueue(ready_list, dequeue(SQ))这个新版本的问题是什么?解释它与第 13.4.1 节中的提示和绝对值问题有何关联

 

  

 



 

 

  

13.20 In Example 13.38 (Figure 13.15), suppose we replaced the middle four lines of procedure P with

 if S.N = 0

  sleep_on(S.Q)

 S.N -:= 1

and the middle four lines of procedure V with

 S.N +:= 1

 if S.Q is nonempty

  enqueue(ready_list, dequeue(S.Q))

What is the problem with this new version? Explain how it connects to the question of hints and absolutes in Section 13.4.1.

13.21 假设每个监视器都有一个单独的互斥锁,这样不同的线程可以同时在不同的监视器中运行,并且当线程在嵌套调用中wait时,我们希望在内监视器和外监视器上都释放互斥。当线程被唤醒时,它将需要重新获取外锁。我们如何确保它能够这样做?(提示:考虑获取锁的顺序,并准备放弃霍尔语义。有关更多提示,请参阅 Wettstein [ Wet78 ]。)

13.21 Suppose that every monitor has a separate mutual exclusion lock, so that different threads can run in different monitors concurrently, and that we want to release exclusion on both inner and outer monitors when a thread waits in a nested call. When the thread awakens it will need to reacquire the outer locks. How can we ensure its ability to do so? (Hint: Think about the order in which to acquire locks, and be prepared to abandon Hoare semantics. For further hints, see Wettstein [Wet78].)

13.22 说明如何使用条件临界区实现通用信号量,其中所有线程都等待相同条件,从而避免无效唤醒的开销。

13.22 Show how general semaphores can be implemented with conditional critical regions in which all threads wait for the same condition, thereby avoiding the overhead of unproductive wake-ups.

13.23 使用 Ada 95 的受保护对象机制为有界缓冲区编写代码。

13.23 Write code for a bounded buffer using the protected object mechanism of Ada 95.

13.24使用 synchronized语句或方法重复上一个Java练习。尽量使你的解决方案尽可能简单和概念清晰。你可能想要使用notifyAll

13.24 Repeat the previous exercise in Java using synchronized statements or methods. Try to make your solution as simple and conceptually clear as possible. You will probably want to use notifyAll.

13.25 为上一个练习给出一个更有效的解决方案,避免使用notifyAll。(警告:很容易观察到缓冲区不可能同时为满和空,因此假设等待线程要么全部是生产者,要么全部是消费者。然而,情况不一定如此:如果缓冲区成为哪怕是暂时的性能瓶颈,则可能会有任意数量的等待线程,包括生产者和消费者。)

13.25 Give a more efficient solution to the previous exercise that avoids the use of notifyAll. (Warning: It is tempting to observe that the buffer can never be both full and empty at the same time, and to assume therefore that waiting threads are either all producers or all consumers. This need not be the case, however: if the buffer ever becomes even a temporary performance bottleneck, there may be an arbitrary number of waiting threads, including both producers and consumers.)

13.26使用Java  Lock变量重复上面的练习。

13.26 Repeat the previous exercise using Java Lock variables.

13.27 解释如何使用边栏 10.3 中简要提到的逃逸分析来降低Java 中某些同步语句和方法的成本。

13.27 Explain how escape analysis, mentioned briefly in Sidebar 10.3, could be used to reduce the cost of certain synchronized statements and methods in Java.

13.28 哲学家就餐问题[ Dij72 ] 是一个经典的同步练习(图 13.20)。五位哲学家围坐在一张圆桌旁。桌子中央放着一大盘意大利面条。每位哲学家反复思考一会儿,然后吃一会儿,时间间隔由他或她自己选择。每对相邻的哲学家之间的桌子上有一把餐叉。要吃饭,哲学家需要两把相邻的餐叉:左边的和右边的。因为他们共用一个餐叉,所以相邻的哲学家不能同时吃饭。

写出哲学家就餐问题的解决方案,其中每个哲学家由一个进程表示,餐叉由共享数据表示。使用信号量、监视器或条件临界区同步对餐叉的访问。尝试最大化并发性。

13.28 The dining philosophers problem [Dij72] is a classic exercise in synchronization (Figure 13.20). Five philosophers sit around a circular table. In the center is a large communal plate of spaghetti. Each philosopher repeatedly thinks for a while and then eats for a while, at intervals of his or her own choosing. On the table between each pair of adjacent philosophers is a single fork. To eat, a philosopher requires both adjacent forks: the one on the left and the one on the right. Because they share a fork, adjacent philosophers cannot eat simultaneously.

Write a solution to the dining philosophers problem in which each philosopher is represented by a process and the forks are represented by shared data. Synchronize access to the forks using semaphores, monitors, or conditional critical regions. Try to maximize concurrency.

f13-20-9780124104099
图 13.20 就餐的哲学家。饥饿的哲学家必须争夺左右两边的餐叉才能吃饭。

13.29 在上一个练习中,你可能已经注意到,就餐的哲学家很容易陷入死锁。人们不得不担心,五个哲学家可能会同时拿起右手边的叉子,然后永远等着左手边的邻居吃完饭。

讨论你能想到的解决死锁问题的尽可能多的策略。你能描述一个解决方案,证明任何哲学家都不可能永远挨饿吗?你能描述一个在严格意义上公平的解决方案吗(即,从长远来看,没有一个哲​​学家比其他哲学家有更多吃饭的机会)?有关特别优雅的解决方案,请参阅 Chandy 和 Misra 的论文 [ CM84 ]。

13.29 In the previous exercise you may have noticed that the dining philosophers are prone to deadlock. One has to worry about the possibility that all five of them will pick up their right-hand forks simultaneously, and then wait forever for their left-hand neighbors to finish eating.

Discuss as many strategies as you can think of to address the deadlock problem. Can you describe a solution in which it is provably impossible for any philosopher to go hungry forever? Can you describe a solution that is fair in a strong sense of the word (i.e., in which no one philosopher gets more chance to eat than some other over the long term)? For a particularly elegant solution, see the paper by Chandy and Misra [CM84].

13.30 在某些并发编程系统中,全局变量由所有线程共享。在其他系统中,每个新创建的线程都有全局变量的单独副本,通常将其初始化为创建线程的全局变量的值。在这种私有全局变量方法下,共享数据必须从特殊堆中分配。在其他编程系统中,程序员可以指定哪些全局变量是私有的,哪些是共享的。

讨论私有全局变量和共享全局变量之间的权衡。对于哪种程序,你更愿意使用哪种全局变量?你将如何实现每种全局变量?有些选项比其他选项更难实现吗?你的答案在多大程度上取决于操作系统提供的进程的性质?

13.30 In some concurrent programming systems, global variables are shared by all threads. In others, each newly created thread has a separate copy of the global variables, commonly initialized to the values of the globals of the creating thread. Under this private globals approach, shared data must be allocated from a special heap. In still other programming systems, the programmer can specify which global variables are to be private and which are to be shared.

Discuss the tradeoffs between private and shared global variables. Which would you prefer to have available, for which sorts of programs? How would you implement each? Are some options harder to implement than others? To what extent do your answers depend on the nature of processes provided by the operating system?

13.31用 Ja​​va 重写示例 13.51

13.31 Rewrite Example 13.51 in Java.

13.32逻辑语言中的 AND并行类似于函数式语言(例如 Multilisp)中参数的并行计算。OR 并行有类似的类似物吗(提示:考虑特殊形式 [第 11.5 节]。)您能建议一种在 Multilisp 中获得OR并行效果的方法吗?

13.32 AND parallelism in logic languages is analogous to the parallel evaluation of arguments in a functional language (e.g., Multilisp). Does OR parallelism have a similar analog? (Hint: Think about special forms [Section 11.5].) Can you suggest a way to obtain the effect of OR parallelism in Multilisp?

13.33 第 13.4.5 节中,我们声称Prolog 中的AND并行和OR并行都是有问题的,因为它们没有遵循语言语义所要求的确定性搜索顺序。详细说明这一说法。具体可能出现什么问题?

13.33 In Section 13.4.5 we claimed that both AND parallelism and OR parallelism were problematic in Prolog, because they failed to adhere to the deterministic search order required by language semantics. Elaborate on this claim. What specifically can go wrong?

13-02-9780124104099 13.34–13.38 更深入。

13.34–13.38  In More Depth.

13.8 探索

13.8 Explorations

13.39  x86 指令集的 MMX、SSE 和 AVX 扩展以及 Power 指令集的 AltiVec 扩展使矢量运算可用于通用代码。了解这些指令并研究它们的历史。它们用于哪种类型的代码?它们与矢量超级计算机有何关系?与现代图形处理器有何关系?

13.39 The MMX, SSE, and AVX extensions to the x86 instruction set and the AltiVec extensions to the Power instruction set make vector operations available to general-purpose code. Learn about these instructions and research their history. What sorts of code are they used for? How are they related to vector supercomputers? To modern graphics processors?

13.40  “500 强”名单(top500.org维护着世界上 500 台最强大的计算机的长期信息,这些计算机是根据 Linpack 性能基准进行测量的。浏览该网站。特别注意所部署机器类型的历史趋势。你能解释这些趋势吗?你能找到多少超级计算机技术进入主流和反之亦然的案例?

13.40 The “Top 500” list (top500.org) maintains information, over time, on the 500 most powerful computers in the world, as measured on the Linpack performance benchmark. Explore the site. Pay particular attention to the historical trends in the kinds of machines deployed. Can you explain these trends? How many cases can you find of supercomputer technology moving into the mainstream, and vice versa?

13.41 第 13.3.3 节中,我们指出不同的处理器提供不同级别的内存一致性和不同的机制来在需要时强制进行额外排序。了解有关这些硬件内存模型的更多信息。您可能希望从 Adve 和 Gharachorloo 的教程 [ AG96 ]开始。

13.41 In Section 13.3.3 we noted that different processors provide different levels of memory consistency and different mechanisms to force additional ordering when needed. Learn more about these hardware memory models. You might want to start with the tutorial by Adve and Gharachorloo [AG96].

13.42 在第 13.3.3和13.4.3中,我们对 Java 和 C++ 内存模型进行了非常高级的总结。了解它们的细节。同时研究 Ada 和 C# 的(更松散指定的)模型。它们之间有何区别?它们在各种实际机器上的实现效率如何?实现者面临哪些挑战?对于 Java,探索语言原始定义中围绕内存模型出现的争议(在 Java 5 中更新 - 有关讨论,请参阅 Manson 等人的论文 [ MPA05 ])。对于 C++,特别注意在原子变量的加载和存储上指定弱一致性的能力。

13.42 In Sections 13.3.3 and 13.4.3 we presented a very high-level summary of the Java and C++ memory models. Learn their details. Also investigate the (more loosely specified) models of Ada and C#. How do these compare? How efficiently can each be implemented on various real machines? What are the challenges for implementors? For Java, explore the controversy that arose around the memory model in the original definition of the language (updated in Java 5—see the paper by Manson et al. [MPA05] for a discussion). For C++, pay particular attention to the ability to specify weakened consistency on loads and stores of atomic variables.

13.43 13.3.2 节中,我们简要介绍了非阻塞并发数据结构的设计,这种结构无需锁即可正常工作。了解有关此主题的更多信息。编写正确的非阻塞代码有多难?与基于锁的代码相比,其性能如何?您可能希望从 Michael [ MS98 ] 和 Sundell [ Sun04 ] 的工作开始。要获得更理论的基础,请从 Herlihy 关于等待自由的原始文章[ Her91 ] 和较新的阻塞自由概念[ HLM03 ]开始,或者查看 Herlihy 和 Shavit [ HS12 ] 的文本。

13.43 In Section 13.3.2 we presented a brief introduction to the design of nonblocking concurrent data structures, which work correctly without locks. Learn more about this topic. How hard is it to write correct nonblocking code? How does the performance compare to that of lock-based code? You might want to start with the work of Michael [MS98] and Sundell [Sun04]. For a more theoretical foundation, start with Herlihy's original article on wait freedom [Her91] and the more recent concept of obstruction freedom [HLM03], or check out the text by Herlihy and Shavit [HS12].

13.44 作为对读写锁的可能改进,了解序列锁[ Lam05 ] 和RCU(读取-复制更新)同步习惯用法 [ MAK + 01 ]。这两者都在操作系统社区中被广泛使用。讨论将它们应用于“非专家”编写的代码所涉及的挑战。

13.44 As possible improvements to reader-writer locks, learn about sequence locks [Lam05] and the RCU (read-copy update) synchronization idiom [MAK+01]. Both of these are heavily used in the operating systems community. Discuss the challenges involved in applying them to code written by “nonexperts.”

13.45 第一个软件事务内存系统源于对非阻塞并发数据结构的研究,并且实际上是非阻塞的。然而,大多数最新系统都是基于锁的。请阅读 Ennals [ Enn06 ] 的立场文件以及 Marathe 和 Moir [ MM08 ] 和 Tabba 等人 [ TWGM07 ]的最新论文。您怎么看?TM 系统应该是非阻塞的吗?

13.45 The first software transactional memory systems grew out of work on nonblocking concurrent data structures, and were in fact nonblocking. Most recent systems, however, are lock based. Read the position paper by Ennals [Enn06] and the more recent papers of Marathe and Moir [MM08] and Tabba et al. [TWGM07]. What do you think? Should TM systems be nonblocking?

13.46 最广泛使用的语言级事务内存是Haskell 的STM monad,由 Glasgow Haskell 编译器和运行时系统支持。阅读其语法和实现 [ HMPH05 ]。付费特别关注重试orElse机制。讨论它们与条件临界区的相似之处和优势。

13.46 The most widely used language-level transactional memory is the STM monad of Haskell, supported by the Glasgow Haskell compiler and runtime system. Read up on its syntax and implementation [HMPH05]. Pay particular attention to the retry and orElse mechanisms. Discuss their similarities to—and advantages over—conditional critical regions.

13.47 研究一些您喜欢的库包的文档(可能是 C 和 C++ 标准库,或者 .NET 和 Java 库,或者许多可用的数学计算包)。哪些例程可以安全地从多线程程序中调用?哪些不能?是什么导致了这种差异?为什么不让所有例程都是线程安全的?

13.47 Study the documentation for some of your favorite library packages (the C and C++ standard libraries, perhaps, or the .NET and Java libraries, or the many available packages for mathematical computing). Which routines can safely be called from a multithreaded program? Which cannot? What accounts for the difference? Why not make all routines thread safe?

13.48 详细研究几种并发语言。下载实现并使用它们编写几种不同类型的并行程序。(例如,你可以尝试康威生命游戏、德劳内三角剖分和高斯消元法;所有这些的描述都可以在网上轻松找到。)写一篇关于你经验的论文。哪些行之有效?哪些行不通?你可能考虑的语言包括 Ada、C#、Cilk、Erlang、Go、Haskell、Java、Modula-3、Occam、Rust、SR 和 Swift。所有这些的参考资料都可以在附录 A中找到。

13.48 Undertake a detailed study of several concurrent languages. Download implementations and use them to write parallel programs of several different sorts. (You might, for example, try Conway's Game of Life, Delaunay Triangulation, and Gaussian Elimination; descriptions of all of these can easily be found on the Web.) Write a paper about your experience. What worked well? What didn't? Languages you might consider include Ada, C#, Cilk, Erlang, Go, Haskell, Java, Modula-3, Occam, Rust, SR, and Swift. References for all of these can be found in Appendix A.

13.49 了解本章末尾参考文献中讨论的超级计算语言:Co-Array Fortran、Titanium 和 UPC;Chapel、Fortress 和 X10。这些语言彼此之间有何区别?与 MPI 和 OpenMP 相比如何?与不太注重“高端”计算的语言相比如何?

13.49 Learn about the supercomputing languages discussed in the Bibliographic Notes at the end of the chapter: Co-Array Fortran, Titanium, and UPC; and Chapel, Fortress, and X10. How do these compare to one another? To MPI and OpenMP? To languages with less of a focus on “high-end” computing?

13.50 本着上一个问题的精神,了解 SHMEM 库包,它最初由 Cray, Inc. 的 Robert Numrich 开发,现在标准化为 OpenSHMEM ( openshmem.org )。SHMEM 广泛用于大规模多处理器和集群上的并行编程。它被描述为共享内存和消息传递的结合。这种描述合理吗?在什么情况下,shmem程序有望胜过 MPI 或 OpenMP 中的解决方案?

13.50 In the spirit of the previous question, learn about the SHMEM library package, originally developed by Robert Numrich of Cray, Inc., and now standardized as OpenSHMEM (openshmem.org). SHMEM is widely used for parallel programming on both large-scale multiprocessors and clusters. It has been characterized as a cross between shared memory and message passing. Is this a fair characterization? Under what circumstances might a shmem program be expected to outperform solutions in MPI or OpenMP?

13.51 本章的大部分内容都致力于并行程序中竞争的管理。这项任务的复杂性提出了一个诱人的问题:是否有可能设计一种功能强大、广泛使用的并发编程语言,并且其中的程序本质上不存在竞争?对于(大部分)肯定的答案,有三种截然不同的看法,请参阅 Edward Lee [ Lee06 ] 的著作、Haskell 的各种并发方言 [ NA01JGF96 ] 和确定性并行 Java (DPJ) [ BAD + 09 ]。

13.51 Much of this chapter has been devoted to the management of races in parallel programs. The complexity of the task suggests a tantalizing question: is it possible to design a concurrent programming language that is powerful enough to be widely useful, and in which programs are inherently race-free? For three very different takes on a (mostly) affirmative answer, see the work of Edward Lee [Lee06], the various concurrent dialects of Haskell [NA01, JGF96], and Deterministic Parallel Java (DPJ) [BAD+09].

13-02-9780124104099 13.52–13.54 更深入。

13.52–13.54  In More Depth.

13.9 书目注释

13.9 Bibliographic Notes

早期并发性研究主要源于 Dijkstra 的两篇文章 [ Dij68aDij72 ]。Andrews 和 Schneider [ AS83 ] 在 20 世纪 80 年代初对该领域进行了精彩的概述。Holt 等人的 [ HGLS78 ] 是许多并发性和同步性经典问题的有用参考。

Much of the early study of concurrency stems from a pair of articles by Dijkstra [Dij68a, Dij72]. Andrews and Schneider [AS83] provided an excellent snapshot of the field in the early 1980s. Holt et al. [HGLS78] is a useful reference for many of the classic problems in concurrency and synchronization.

彼得森双进程同步算法出现在一篇非常优雅且可读的两页论文中 [ Pet81 ]。Lamport 于 1978 年发表的有关“分布式系统中的时间、时钟和事件顺序”的文章 [ Lam78 ] 令人信服地指出,全局时间的概念无法得到很好的定义,因此分布式算法必须基于各个进程之间的因果关系。读写锁归功于 Courtois、Heymans 和 Parnas [ CHP71 ]。Java 7 phaser 部分受到 Shirako 等人的工作启发 [ SPSS08 ]。Mellor-Crummey 和 Scott [ MCS91 ] 调查了主要的忙等待同步算法,并引入了可在无争用的情况下扩展到大型机器的锁和屏障。

Peterson's two-process synchronization algorithm appears in a remarkably elegant and readable two-page paper [Pet81]. Lamport's 1978 article on “Time, Clocks, and the Ordering of Events in a Distributed System” [Lam78] argued convincingly that the notion of global time cannot be well defined, and that distributed algorithms must therefore be based on causal happens before relationships among individual processes. Reader–writer locks are due to Courtois, Heymans, and Parnas [CHP71]. Java 7 phasers were inspired in part by the work of Shirako et al. [SPSS08]. Mellor-Crummey and Scott [MCS91] survey the principal busy-wait synchronization algorithms and introduce locks and barriers that scale without contention to very large machines.

Herlihy [ Her91 ]撰写了有关无锁同步的开创性论文。Michael 和 Scott [ MS96 ]提出了示例 13.30中的非阻塞并发队列。Herlihy 和 Shavit [ HS12 ] 以及 Scott [ Sco13 ] 则提供了现代化的、一本书长度的有关同步和并发数据结构的介绍。Adve 和 Gharachorloo 引入了硬件内存模型的概念 [ AG96 ]。Pugh 解释了原始 Java 内存模型 [ Pug00 ]中存在的问题;Manson、Pugh 和 Adve [ MPA05 ] 描述了修订后的模型。Boehm 和 Adve [ BA08 ] 描述了 C++11 的内存模型。Boehm 令人信服地指出,如果没有编译器支持,线程就无法正确实现 [ Boe05 ]。有关事务内存的原始论文由 Herlihy 和 Moss [ HM93 ]撰写。 Harris、Larus 和 Rajwar 于 2010 年底对该领域进行了一本书长度的调查 [ HLR10 ]。Larus 和 Kozyrakis 提供了更简短的概述 [ LK08 ]。

The seminal paper on lock-free synchronization is that of Herlihy [Her91]. The nonblocking concurrent queue of Example 13.30 is due to Michael and Scott [MS96]. Herlihy and Shavit [HS12] and Scott [Sco13] provide modern, book-length coverage of synchronization and concurrent data structures. Adve and Gharachorloo introduce the notion of hardware memory models [AG96]. Pugh explains the problems with the original Java Memory Model [Pug00]; the revised model is described by Manson, Pugh, and Adve [MPA05]. The memory model for C++11 is described by Boehm and Adve [BA08]. Boehm has argued convincingly that threads cannot be implemented correctly without compiler support [Boe05]. The original paper on transactional memory is by Herlihy and Moss [HM93]. Harris, Larus, and Rajwar provide a book-length survey of the field as of late 2010 [HLR10]. Larus and Kozyrakis provide a briefer overview [LK08].

两代用于高端计算的并行语言影响深远。分区全局地址空间 (PGAS) 语言包括 Co-Array Fortran (CAF)、统一并行 C (UPC) 和 Titanium(Java 的一种方言)。它们支持变量的单一全局名称空间,但采用“额外维度”寻址来访问不在本地核心上的数据。CAF 的大部分功能已被 Fortran 2008 采用。所谓的 HPCS 语言(Chapel、Fortress 和 X10)以 PGAS 语言的经验为基础,但针对更广泛的硬件、应用程序和并行风格。这三种语言都包含事务功能。对于所有这些语言,网络搜索可能是当前信息的最佳来源。

Two recent generations of parallel languages for high-end computing have been highly influential. The Partitioned Global Address Space (PGAS) languages include Co-Array Fortran (CAF), Unified Parallel C (UPC), and Titanium (a dialect of Java). They support a single global name space for variables, but employ an “extra dimension” of addressing to access data not on the local core. Much of the functionality of CAF has been adopted into Fortran 2008. The so-called HPCS languages—Chapel, Fortress, and X10—build on experience with the PGAS languages, but target a broader range of hardware, applications, and styles of parallelism. All three include transactional features. For all of these, a web search is probably the best source of current information.

MPI [ Mes12 ] 已在各种文章和书籍中有所介绍。最新版本从早期的竞争系统 PVM(并行虚拟机)[ Sun90GBD + 94 ] 中汲取了多项功能。在 Nelson 的博士研究 [ BN84 ] 之后,远程过程调用受到越来越多的关注。开放网络计算 RPC 标准已在 Internet RFC 编号 1831 [ Sri95 ]中有所介绍。RPC 还构成了 CORBA、COM、JavaBeans 和 SOAP 等高级标准的基础。

MPI [Mes12] is documented in a variety of articles and books. The latest version draws several features from an earlier, competing system known as PVM (Parallel Virtual Machine) [Sun90, GBD+94]. Remote procedure call received increasing attention in the wake of Nelson's doctoral research [BN84]. The Open Network Computing RPC standard is documented in Internet RFC number 1831 [Sri95]. RPC also forms the basis of such higher-level standards as CORBA, COM, JavaBeans, and SOAP.

软件分布式共享内存(S-DSM)最初由李在其博士论文中提出 [ LH89 ]。莱斯大学的 TreadMarks 系统被广泛认为是各种实现中最成熟、最强大的 [ ACD + 96 ]。

Software distributed shared memory (S-DSM) was originally proposed by Li as part of his doctoral research [LH89]. The TreadMarks system from Rice University was widely considered the most mature and robust of the various implementations [ACD+96].


1理想情况下,我们可能希望编译器能够自动解决这个问题,但一般情况下独立性问题是无法判定的。

1 Ideally, we might like the compiler to figure this out automatically, but the problem of independence is undecidable in the general case.

2 Leslie Lamport (1941-) 对并行和分布式计算理论做出了多项开创性贡献,包括同步算法、“先发生”因果关系概念、拜占庭协议、Paxos 共识算法和动作时序逻辑。他还创建了 L A T E X 宏包,本书就是用该宏包排版的。他于 2013 年获得 ACM 图灵奖。

2 Leslie Lamport (1941–) has made a variety of seminal contributions to the theory of parallel and distributed computing, including synchronization algorithms, the notion of “happens-before” causality, Byzantine agreement, the Paxos consensus algorithm, and the temporal logic ofactions. He also created the LATEX macro package, with which this book was typeset. He received the ACM Turing Award in 2013.

3栅栏有时也称为内存屏障。它们与第 8.5.3 节(“跟踪收集”)中的垃圾收集屏障、第 13.3.1 节中的同步屏障或第 C-15.2.1 节中的 RTL 屏障无关。

3 Fences are also sometimes known as memory barriers. They are unrelated to the garbage collection barriers of Section 8.5.3 (“Tracing Collection”), the synchronization barriers of Section 13.3.1, or the RTL barriers of Section C-15.2.1.

4 P代表荷兰语passeren(传递)或proberen(测试); V代表vrijgeven(释放)或verhogen(增加)。为了保持联系,英语使用者可能希望将P视为“暂停”,因为如果信号量计数为负,线程将在P操作处暂停。Algol 68分别将PV操作称为 down 和 up。

4 P stands for the Dutch word passeren (to pass) or proberen (to test); V stands for vrijgeven (to release) or verhogen (to increment). To keep them straight, speakers of English may wish to think of P as standing for “pause,” since a thread will pause at a P operation if the semaphore count is negative. Algol 68 calls the P and V operations down and up, respectively.

5 Mesa 与 Smalltalk 和 Interlisp 一起,是 20 世纪 70 年代施乐公司帕洛阿尔托研究中心诞生的三种有影响力的语言之一。这三种语言都是在 Alto 个人电脑上开发的,该电脑开创了位图显示、鼠标、图形用户界面、所见即所得编辑、以太网和激光打印机等概念。Mesa 项目由 Butler Lampson(1943-)领导,他在后来的 Euclid 和 Cedar 开发中也发挥了关键作用。由于他对个人和分布式计算的贡献,Lampson 于 1992 年获得了 ACM 图灵奖。

5 Together with Smalltalk and Interlisp, Mesa was one of three influential languages to emerge from Xerox's Palo Alto Research Center in the 1970s. All three were developed on the Alto personal computer, which pioneered such concepts as the bitmapped display, the mouse, the graphical user interface, WYSIWYG editing, Ethernet networking, and the laser printer. The Mesa project was led by Butler Lampson (1943–), who played a key role in the later development of Euclid and Cedar as well. For his contributions to personal and distributed computing, Lampson received the ACM Turing Award in 1992.

14

脚本语言

Scripting Languages

传统编程语言主要用于构建独立的应用程序:接受某种输入、以某种易于理解的方式对其进行操作并生成适当输出的程序。但计算机的大多数实际用途都需要多个程序的协调。例如,大型机构工资系统必须处理来自读卡器、扫描纸质表格和手动(键盘)输入的时间报告数据;执行数千个数据库查询;执行数百条法律和机构规则;为记录保存、审计和纳税申报目的创建广泛的“纸质记录”;打印薪水支票;并与世界各地的服务器通信以进行在线直接存款、税收预扣、退休金积累、医疗保险等。这些任务可能涉及数十或数百个单独可执行的程序。这些程序之间的协调肯定需要测试和条件、循环、变量和类型、子例程和抽象——传统语言在应用程序内提供的逻辑工具相同。

Traditional programming languages are intended primarily for the construction of self-contained applications: programs that accept some sort of input, manipulate it in some well-understood way, and generate appropriate output. But most actual uses of computers require the coordination of multiple programs. A large institutional payroll system, for example, must process time-reporting data from card readers, scanned paper forms, and manual (keyboard) entry; execute thousands of database queries; enforce hundreds of legal and institutional rules; create an extensive “paper trail” for record-keeping, auditing, and tax preparation purposes; print paychecks; and communicate with servers around the world for on-line direct deposit, tax withholding, retirement accumulation, medical insurance, and so on. These tasks are likely to involve dozens or hundreds of separately executable programs. Coordination among these programs is certain to require tests and conditionals, loops, variables and types, subroutines and abstractions—the same sorts of logical tools that a conventional language provides inside an application.

在更小的范围内,平面设计师或摄影记者可能会定期从数码相机下载图片;将它们转换为喜欢的格式;旋转垂直拍摄的图片;对它们进行下采样以创建可浏览的缩略图版本;按日期、主题和颜色直方图对它们进行索引;将它们备份到远程存档;然后重新初始化相机的内存。手动执行这些步骤可能既繁琐又容易出错。同样,创建动态网页可能需要身份验证和授权、数据库查找、图像处理、远程通信以及 HTML 文本的读写。所有这些场景都表明需要协调其他程序的程序。

On a much smaller scale, a graphic artist or photojournalist may routinely download pictures from a digital camera; convert them to a favorite format; rotate the pictures that were shot in vertical orientation; down-sample them to create browsable thumbnail versions; index them by date, subject, and color histogram; back them up to a remote archive; and then reinitialize the camera's memory. Performing these steps by hand is likely to be both tedious and error-prone. In a similar vein, the creation of a dynamic web page may require authentication and authorization, database lookup, image manipulation, remote communication, and the reading and writing of HTML text. All these scenarios suggest a need for programs that coordinate other programs.

当然,用 Java、C 或其他传统语言编写协调代码是可能的,但这并不总是那么容易。传统语言往往强调效率、可维护性、可移植性和静态错误检测。它们的类型系统往往围绕硬件级概念构建,如固定大小的整数、浮点数、字符和数组。相比之下,脚本语言往往强调灵活性、快速开发、本地定制和动态(运行时)检查。同样,它们的类型系统倾向于包含诸如表、模式、列表和文件之类的高级概念。

It is of course possible to write coordination code in Java, C, or some other conventional language, but it isn't always easy. Conventional languages tend to stress efficiency, maintainability, portability, and the static detection of errors. Their type systems tend to be built around such hardware-level concepts as fixed-size integers, floating-point numbers, characters, and arrays. By contrast scripting languages tend to stress flexibility, rapid development, local customization, and dynamic (run-time) checking. Their type systems, likewise, tend to embrace such high-level concepts as tables, patterns, lists, and files.

通用脚本语言(例如 Perl、Python 和 Ruby)有时被称为粘合语言,因为它们最初设计用于将现有程序“粘合”在一起以构建更大的系统。随着万维网的发展,脚本语言已成为在服务器和客户端浏览器上生成动态内容的标准方式。它们还被广泛用于定制或扩展编辑器、电子表格、游戏和演示工具等“可编写脚本”系统的功能。

General-purpose scripting languages like Perl, Python, and Ruby are sometimes called glue languages, because they were originally designed to “glue” existing programs together to build a larger system. With the growth of the World Wide Web, scripting languages have become the standard way to generate dynamic content, both on servers and with the client browser. They are also widely used to customize or extend the functionality of such “scriptable” systems as editors, spreadsheets, games, and presentation tools.

我们将在14.1 节中更详细地讨论脚本的历史和性质。然后,我们将在14.2 节中讨论脚本广泛使用的一些问题领域。这些包括命令解释 (shell)、文本处理和报告生成、数学和统计、通用程序协调以及配置和扩展。在14.3 节中,我们将讨论万维网上使用的几种脚本形式,包括 CGI 脚本、嵌入在网页中的脚本的服务器端和客户端处理、Java 小程序以及(在配套站点上)XSLT。最后,在14.4 节中,我们将讨论许多脚本语言共有的一些更有趣的语言特性,这些特性使它们有别于更传统的“主流”同类语言。我们将特别讨论命名、作用域和类型;字符串和模式操作;以及高级结构化数据。我们不会详细介绍任何一种脚本语言,但会考虑几种脚本语言的具体示例。与本书的大部分内容一样,我们将重点介绍底层概念。

We consider the history and nature of scripting in more detail in Section 14.1. We then turn in Section 14.2 to some of the problem domains in which scripting is widely used. These include command interpretation (shells), text processing and report generation, mathematics and statistics, general-purpose program coordination, and configuration and extension. In Section 14.3 we consider several forms of scripting used on the World Wide Web, including CGI scripts, server- and client-side processing of scripts embedded in web pages, Java applets, and (on the companion site) XSLT. Finally, in Section 14.4, we consider some of the more interesting language features, common to many scripting languages, that distinguish them from their more traditional “mainstream” cousins. We look in particular at naming, scoping, and typing; string and pattern manipulation; and high-level structured data. We will not provide a detailed introduction to any one scripting language, though we will consider concrete examples in several. As in most of this book, the emphasis will be on underlying concepts.

14.1 什么是脚本语言?

14.1 What Is a Scripting Language?

现代脚本语言有两大祖先。第一组是传统批处理和“终端”(命令行)计算的命令解释器或“shell”。另一组是用于文本处理和报告生成的各种工具。第一组的例子包括 IBM 的 JCL、MS-DOS命令解释器以及 Unix shcsh shell 系列。第二组的例子包括 IBM 的 RPG 以及 Unix 的sedawk。从这些语言发展而来的是 Rexx(IBM 的“重组扩展执行器”,可追溯到 1979 年)和 Perl(最初由 Larry Wall 在 1980 年代后期设计,至今仍然是使用最广泛的通用脚本语言之一)。其他通用脚本语言包括 Python、Ruby、PowerShell(适用于 Windows)和 AppleScript(适用于 Mac)。

Modern scripting languages have two principal sets of ancestors. In one set are the command interpreters or “shells” of traditional batch and “terminal” (command-line) computing. In the other set are various tools for text processing and report generation. Examples in the first set include IBM's JCL, the MS-DOS command interpreter, and the Unix sh and csh shell families. Examples in the second set include IBM's RPG and Unix's sed and awk. From these evolved Rexx, IBM's “Restructured Extended Executor,” which dates from 1979, and Perl, originally devised by Larry Wall in the late 1980s, and still one of the most widely used general-purpose scripting languages. Other general-purpose scripting languages include Python, Ruby, PowerShell (for Windows), and AppleScript (for the Mac).

随着 20 世纪 90 年代末万维网的发展,Perl 被广泛用于“服务器端”Web 脚本,即 Web 服务器执行程序(在服务器的机器上)来生成页面内容。早期的 Web 脚本爱好者 Rasmus Lerdorf 就是其中之一,他创建了一组脚本来跟踪对其个人主页的访问。这些脚本最初是用 Perl 编写的,但很快被重新设计为一种成熟且独立的语言,并演变为 PHP,现在它是服务器端 Web 脚本最流行的平台。PHP 的竞争对手包括 JSP(Java Server Pages)、Ruby on Rails,以及 Microsoft 平台上的对于在客户端计算机上编写脚本,所有主流浏览器都实现了 JavaScript,这是 Netscape 公司在 20 世纪 90 年代中期开发的一种语言,并于 1999 年由 ECMA(欧洲标准机构)进行了标准化 [ ECM11 ]。

With the growth of the World Wide Web in the late 1990s, Perl was widely adopted for “server-side” web scripting, in which a web server executes a program (on the server's machine) to generate the content of a page. One early web-scripting enthusiast was Rasmus Lerdorf, who created a collection of scripts to track access to his personal home page. Originally written in Perl but soon redesigned as a full-fledged and independent language, these scripts evolved into PHP, now the most popular platform for server-side web scripting. PHP competitors include JSP (Java Server Pages), Ruby on Rails, and, on Microsoft platforms, PowerShell. For scripting on the client computer, all major browsers implement JavaScript, a language developed by Netscape Corporation in the mid 1990s, and standardized by ECMA (the European standards body) in 1999 [ECM11].

在有关脚本的经典论文 [ Ous98 ] 中,Tcl 的创建者 John Ousterhout 提出:“脚本语言假设其他语言中已经存在一组有用的组件。它们的目的不是用于从头编写应用程序,而是用于组合组件。”Ousterhout 设想了这样一个未来:程序员将越来越依赖脚本语言来构建系统的顶层结构,而清晰度、可重用性和易于开发性是其中的关键。他认为,C、C++ 或 Java 等传统“系统语言”将用于自包含、可重用的系统组件,这些组件强调复杂的算法或执行速度。他认为,这是一个至今看来仍然合理的一般经验法则,即使用脚本语言开发代码的速度可以提高 5 到 10 倍,但使用传统系统语言运行代码的速度可以提高 10 到 20 倍。

In a classic paper on scripting [Ous98], John Ousterhout, the creator of Tcl, suggested that “Scripting languages assume that a collection of useful components already exist in other languages. They are intended not for writing applications from scratch but rather for combining components.” Ousterhout envisioned a future in which programmers would increasingly rely on scripting languages for the top-level structure of their systems, where clarity, reusability, and ease of development are crucial. Traditional “systems languages” like C, C++, or Java, he argued, would be used for self-contained, reusable system components, which emphasize complex algorithms or execution speed. As a general rule of thumb that still seems reasonable today, he suggested that code could be developed 5 to 10 times faster in a scripting language, but would run 10 to 20 times faster in a traditional systems language.

一些作者将术语“脚本”保留用于协调多个程序的粘合语言。但在常见用法中,脚本是一个更广泛和更模糊的概念,不仅包含 Web 脚本,还包括扩展语言。这些通常嵌入在某些较大的宿主程序中,然后可以控制这些程序。许多读者都熟悉 Microsoft Office 和相关应用程序的 Visual Basic“宏”。其他人可能熟悉emacs文本编辑器的基于 Lisp 的扩展语言,或计算机游戏行业中广泛使用的 Lua。其他几种语言,包括 Tcl、Rexx、Python 以及 Scheme 的 Guile 和 Elk 方言,都有旨在嵌入到其他应用程序中的实现。同样,一些广泛使用的商业应用程序也提供了自己的专有扩展语言。对于图形用户界面 (GUI) 编程,最初设计用于 Tcl 的 Tk 工具包已被整合到几种脚本语言中,包括 Perl、Python 和 Ruby。

Some authors reserve the term “scripting” for the glue languages used to coordinate multiple programs. In common usage, however, scripting is a broader and vaguer concept, encompassing not only web scripting but also extension languages. These are typically embedded within some larger host program, which they can then control. Many readers will be familiar with the Visual Basic “macros” of Microsoft Office and related applications. Others may be familiar with the Lisp-based extension language of the emacs text editor, or the widespread use of Lua in the computer gaming industry. Several other languages, including Tcl, Rexx, Python, and the Guile and Elk dialects of Scheme, have implementations designed to be embedded in other applications. In a similar vein, several widely used commercial applications provide their own proprietary extension languages. For graphical user interface (GUI) programming, the Tk toolkit, originally designed for use with Tcl, has been incorporated into several scripting languages, including Perl, Python, and Ruby.

你也可以将 XSLT(可扩展样式表语言转换)视为一种脚本语言,尽管它与本章中讨论的其他语言略有不同。XSLT 是日益壮大的 XML(可扩展标记语言)工具家族的一部分。我们将在第14.3.5 节中进一步讨论它。

One can also view XSLT (extensible stylesheet language transformations) as a scripting language, albeit somewhat different from the others considered in this chapter. XSLT is part of the growing family of XML (extensible markup language) tools. We consider it further in Section 14.3.5.

14.1.1 共同特征

14.1.1 Common Characteristics

虽然很难准确定义脚本语言,但它们往往具有几个共同的特征:

While it is difficult to define scripting languages precisely, there are several characteristics that they tend to have in common:

批处理和交互式都使用。一些脚本语言(尤其是 Perl)有一个编译器,它坚持在产生任何输出之前读取整个源程序。然而,大多数其他语言都愿意逐行编译或解释其输入。Rexx、Python、Tcl、Guile 以及(带有简短的帮助脚本)Ruby 和 Lua 都接受来自键盘的命令。

Both batch and interactive use. A few scripting languages (notably Perl) have a compiler that insists on reading the entire source program before it produces any output. Most other languages, however, are willing to compile or interpret their input line by line. Rexx, Python, Tcl, Guile, and (with short helper scripts) Ruby and Lua will all accept commands from the keyboard.

表达经济。为了支持快速开发和交互式使用,脚本语言往往需要最少的“样板”。有些脚本语言大量使用标点符号和非常短的标识符(Perl 因这一点而臭名昭著),而其他脚本语言(例如 Rexx、Tcl 和 AppleScript)则更倾向于“英语化”,单词很多但标点符号很少。所有脚本语言都试图避免传统语言中常见的大量声明和顶级结构。

Economy of expression. To support both rapid development and interactive use, scripting languages tend to require a minimum of “boilerplate.” Some make heavy use of punctuation and very short identifiers (Perl is notorious for this), while others (e.g., Rexx, Tcl, and AppleScript) tend to be more “English-like,” with lots of words and not much punctuation. All attempt to avoid the extensive declarations and top-level structure common to conventional languages.

例 14.1

Example 14.1

传统语言和脚本语言中的简单程序

Trivial programs in conventional and scripting languages

在 Java 中,一个简单的程序如下所示:

class Hello {

Where a trivial program looks like this in Java:

class Hello {

 公共静态void main(String [] args){

 public static void main(String[] args) {

  System.out.println(“你好,世界!”);

  System.out.println(“Hello, world!”);

 }

 }

}

}

在 Ada 中如下:

and like this in Ada:

与 ada.text_IO;使用 ada.text_IO;

with ada.text_IO; use ada.text_IO;

程序 hello 是

procedure hello is

开始

begin

 放入行(“你好,世界!”);

 put_line(“Hello, world!”);

结束你好;

end hello;

在 Perl、Python 或 Ruby 中,它只是

in Perl, Python, or Ruby it is simply

打印“你好,世界!\n”

print “Hello, world!\n”

缺少声明;作用域规则简单。大多数脚本语言都省去了声明,而是提供了简单的规则来管理名称的作用域。在某些语言(例如 Perl)中,默认情况下所有内容都是全局的;可以使用可选声明将变量限制在嵌套作用域内。在其他语言(例如 PHP 和 Tcl)中,默认情况下所有内容都是本地的;全局变量必须显式导入。Python 采用了一个有趣的规则,即任何被赋值的变量都是赋值出现所在的块的本地变量。需要使用特殊语法才能在周围作用域中赋值给变量。

Lack of declarations; simple scoping rules. Most scripting languages dispense with declarations, and provide simple rules to govern the scope of names. In some languages (e.g., Perl) everything is global by default; optional declarations can be used to limit a variable to a nested scope. In other languages (e.g., PHP and Tcl), everything is local by default; globals must be explicitly imported. Python adopts the interesting rule that any variable that is assigned a value is local to the block in which the assignment appears. Special syntax is required to assign to a variable in a surrounding scope.

设计与实现

Design & Implementation

14.1 编译解释型语言

14.1 Compiling interpreted languages

本章中,我们将多次提到脚本语言的“编译器”。正如我们在示例 1.91.10中看到的,解释器几乎从不使用源代码;前端翻译器首先用某种中间形式替换该源。对于本章中描述的大多数语言的大多数实现,前端足够复杂,值得称为“编译器”。中间形式通常是内部数据结构(例如语法树)或“字节码”表示,让​​人想起 Java 的表示。

Several times in this chapter we will make reference to “the compiler” for a scripting language. As we saw in Examples 1.9 and 1.10, interpreters almost never work with source code; a front-end translator first replaces that source with some sort of intermediate form. For most implementations of most of the languages described in this chapter, the front end is sufficiently complex to deserve the name “compiler.” Intermediate forms are typically internal data structures (e.g., a syntax tree) or “byte-code” representations reminiscent of those of Java.

例 14.2

Example 14.2

Perl 中的强制转换

Coercion in Perl

灵活的动态类型。为了避免声明,大多数脚本语言都是动态类型的。在一些脚本语言(例如 PHP、Python、Ruby 和 Scheme)中,变量的类型在使用前会立即检查。在其他脚本语言(例如 Rexx、Perl 和 Tcl)中,变量在不同的上下文中会有不同的解释。例如,在 Perl 中,程序

Flexible dynamic typing. In keeping with the lack of declarations, most scripting languages are dynamically typed. In some (e.g., PHP, Python, Ruby, and Scheme), the type of a variable is checked immediately prior to use. In others (e.g., Rexx, Perl, and Tcl), a variable will be interpreted differently in different contexts. In Perl, for example, the program

$a =“4”;
打印 $a.3.“\n”;# '.' 是连接
打印 $a + 3 . “\n”;# '+' 表示加法

将打印

will print

43

43

7

7

这种上下文解释类似于强制,不同之处在于,没有必要从“自然”类型概念来转换对象;各种可能的解释可能都同样“自然”。我们将在14.4.3 节中更详细地介绍 Perl 中的上下文。■

This contextual interpretation is similar to coercion, except that there isn't necessarily a notion of the “natural” type from which an object must be converted; the various possible interpretations may all be equally “natural.” We shall have more to say about context in Perl in Section 14.4.3. ■

轻松访问系统设施。大多数编程语言都提供了一种方法,可以要求底层操作系统运行另一个程序,或直接执行某些操作。然而,在脚本语言中,这些请求更为基础,并且有更直接的支持。例如,Perl 提供了 100 多个内置命令,这些命令可以访问操作系统函数,用于输入和输出、文件和目录操作、进程管理、数据库访问、套接字、进程间通信和同步、保护和授权、时钟和网络通信。这些内置命令通常比 C 等语言中相应的库调用更容易使用。

Easy access to system facilities. Most programming languages provide a way to ask the underlying operating system to run another program, or to perform some operation directly. In scripting languages, however, these requests are much more fundamental, and have much more direct support. Perl, for one, provides well over 100 built-in commands that access operating system functions for input and output, file and directory manipulation, process management, database access, sockets, interprocess communication and synchronization, protection and authorization, time-of-day clock, and network communication. These built-in commands are generally a good bit easier to use than corresponding library calls in languages like C.

设计与实现

Design & Implementation

14.2 规范实现

14.2 Canonical implementations

由于脚本语言通常使用解释器实现,因此它们往往易于从一台机器移植到另一台机器 — 比必须编写新代码生成器的编译器要容易得多。如果为编写解释器的语言提供了本机编译器,那么唯一困难的部分(可能确实很困难)是对解释器中提供操作系统接口的部分进行任何必要的修改。

Because they are usually implemented with interpreters, scripting languages tend to be easy to port from one machine to another—substantially easier than compilers for which one must write a new code generator. Given a native compiler for the language in which the interpreter is written, the only difficult part (and it may indeed be difficult) is to implement any necessary modifications to the part of the interpreter that provides the interface to the operating system.

同时,移植解释器的便利性意味着许多脚本语言(包括 Perl、Python 和 Ruby)都具有单一的广泛使用的实现,该实现充当事实上的语言定义。阅读有关 Perl 的书籍时,很难判断一个微妙的程序会如何运行。如有疑问,可能需要“尝试一下”。Rexx 和 JavaScript 可以说是唯一两种广泛使用的脚本语言,它们既有由国际标准机构编纂的正式定义,又有相当数量的独立实现。(Lua [和 Scheme,如果算作脚本] 有详细的参考手册和多种实现,尽管没有得到标准机构的正式认可。Dart 有 ECMA 标准,但截至 2015 年,只有 Google 实现。Sed 、awksh都已由 POSIX [ Int03b ] 标准化,但它们都不能真正被描述为成熟的脚本语言。)

At the same time, the ease of porting an interpreter means that many scripting languages, including Perl, Python, and Ruby, have a single widely used implementation, which serves as the de facto language definition. Reading a book on Perl, it can be difficult to tell how a subtle program will behave. When in doubt, one may need to “try it out.” Rexx and JavaScript are arguably the only widely used scripting languages that have both a formal definition codified by an international standards body and a nontrivial number of independent implementations. (Lua [and Scheme, if you count it as scripting] have detailed reference manuals and multiple implementations, though no formal blessing from a standards body. Dart has an ECMA standard, but as of 2015, only Google implementations. Sed, awk, and sh have all been standardized by POSIX [Int03b], but none of them can really be described as a full-fledged scripting language.)

复杂的模式匹配和字符串操作。为了保持其文本处理和报告生成血统,并方便外部程序处理文本输入和输出,脚本语言往往具有极其丰富的模式匹配、搜索和字符串操作功能。通常,这些功能基于扩展的正则表达式。我们将在第 14.4.2 节中进一步讨论它们。

Sophisticated pattern matching and string manipulation. In keeping with their text processing and report generation ancestry, and to facilitate the manipulation of textual input and output for external programs, scripting languages tend to have extraordinarily rich facilities for pattern matching, search, and string manipulation. Typically these are based on extended regular expressions. We discuss them further in Section 14.4.2.

高级数据类型。集合、包、字典、列表和元组等高级数据类型在传统编程语言的标准库包中越来越常见。一些语言(尤其是 C++)允许用户重新定义标准中缀运算符,使这些类型像更原始的以硬件为中心的类型一样易于使用。脚本语言更进一步,将高级类型构建到语言本身的语法和语义中。例如,在大多数脚本语言中,通常有一个由字符串索引的“数组”,其底层实现基于哈希表。存储总是会被垃圾收集。

High-level data types. High-level data types like sets, bags, dictionaries, lists, and tuples are increasingly common in the standard library packages of conventional programming languages. A few languages (notably C++) allow users to redefine standard infix operators to make these types as easy to use as more primitive, hardware-centric types. Scripting languages go one step further by building high-level types into the syntax and semantics of the language itself. In most scripting languages, for example, it is commonplace to have an “array” that is indexed by character strings, with an underlying implementation based on hash tables. Storage is invariably garbage collected.

当今编程语言中变化最快的大部分是脚本语言。这可以归因于多种原因,包括 Web 的持续增长、开源社区的活力以及创建新脚本语言所需的投资相对较低。Java 或 C# 等编译型工业级语言需要非常庞大的编程团队投入多年时间才能开发出来,而一位才华横溢的设计师独自工作,仅需一两年时间就能开发出可用的新脚本语言实现。

Much of the most rapid change in programming languages today is occurring in scripting languages. This can be attributed to several causes, including the continued growth of the Web, the dynamism of the open-source community, and the comparatively low investment required to create a new scripting language. Where a compiled, industrial-quality language like Java or C# requires a multiyear investment by a very large programming team, a single talented designer, working alone, can create a usable implementation of a new scripting language in only a year or two.

部分由于这种快速变化,较新的脚本语言已经能够融入语言设计中的一些最具创新性的概念。例如,Ruby 具有统一的对象模型(非常类似于 Smalltalk)、真正的迭代器(如 Clu)、lambda 表达式(如 Lisp)、数组切片(如 Fortran 90)、结构化异常处理、多路赋值和反射。Python 也具有许多这些功能,以及 Ruby 所缺乏的一些功能,包括 Haskell 风格的列表推导。

Due in part to this rapid change, newer scripting languages have been able to incorporate some of the most innovative concepts in language design. Ruby, for example, has a uniform object model (much like Smalltalk), true iterators (like Clu), lambda expressions, (like Lisp), array slices (like Fortran 90), structured exception handling, multiway assignment, and reflection. Python has many of these features as well, and a few that Ruby lacks, including Haskell-style list comprehensions.

14.2 问题域

14.2 Problem Domains

一些通用语言(例如 Scheme 和 Visual Basic)被广泛用于脚本编写。相反,一些脚本语言(包括 Perl)Python 和 Ruby 的设计者旨在将其用于通用目的,其功能旨在支持“大规模编程”:模块、单独编译、反射、程序开发环境等。然而,在大多数情况下,脚本语言往往主要用在明确定义的问题领域。我们将在以下小节中讨论其中的一些。

Some general-purpose languages—Scheme and Visual Basic, for example—are widely used for scripting. Conversely, some scripting languages, including Perl, Python, and Ruby, are intended by their designers for general-purpose use, with features intended to support “programming in the large”: modules, separate compilation, reflection, program development environments, and so on. For the most part, however, scripting languages tend to see their principal use in well-defined problem domains. We consider some of these in the following subsections.

14.2.1 Shell(命令)语言

14.2.1 Shell (Command) Languages

在穿孔卡片计算机时代(大概到 20 世纪 70 年代中期),简单的命令语言允许用户“编写”一副卡片的处理程序。例如,卡片组前面的控制卡可能表示即将出现的卡片代表要编译的程序,或者可能是编译器本身的机器语言,或者是已编译并存储在磁盘上的程序的输入。卡片组后面嵌入的控制卡可能会测试最近执行的程序的退出状态,并根据该程序是否成功完成来选择下一步要做什么。然而,鉴于卡片组的线性特性(通常无法备份),批处理的命令语言往往不太复杂。例如,JCL 没有迭代结构。

In the days of punch-card computing (through perhaps the mid 1970s), simple command languages allowed the user to “script” the processing of a card deck. A control card at the front of the deck, for example, might indicate that the upcoming cards represented a program to be compiled, or perhaps machine language for the compiler itself, or input for a program already compiled and stored on disk. A control card embedded later in the deck might test the exit status of the most recently executed program and choose what to do next based on whether that program completed successfully. Given the linear nature of a card deck, however (one can't in general back up), command languages for batch processing tended not to be very sophisticated. JCL, for example, had no iteration constructs.

随着 20 世纪 60 年代和 70 年代早期交互式分时技术的发展,命令语言变得更加复杂。1963 年和 1964 年,Louis Pouzin 为 MIT 的兼容分时系统 (CTSS) 编写了一个简单的命令解释器。1964 年,当开创性的 Multics 系统开始投入工作时,Pouzin 绘制了一种扩展命令语言的设计草图,该语言具有引用和参数传递机制,他为此创造了术语“shell”。随后的实现为 Ken Thompson 在 1973 年设计原始 Unix shell 提供了灵感。20 世纪 70 年代中期,Stephen Bourne 和 John Mashey 分别用控制流和变量扩展了 Thompson shell;Bourne 的设计被采纳为 Unix 标准,取代了 Thompson shell(并命名为 sh)

With the development of interactive timesharing in the 1960s and early 1970s, command languages became much more sophisticated. Louis Pouzin wrote a simple command interpreter for CTSS, the Compatible Time Sharing System at MIT, in 1963 and 1964. When work began on the groundbreaking Multics system in 1964, Pouzin sketched the design of an extended command language, with quoting and argument-passing mechanisms, for which he coined the term “shell.” The subsequent implementation served as inspiration for Ken Thompson in the design of the original Unix shell in 1973. In the mid-1970s, Stephen Bourne and John Mashey separately extended the Thompson shell with control flow and variables; Bourne's design was adopted as the Unix standard, taking the place (and the name) of the Thompson shell, sh.

20 世纪 70 年代末,Bill Joy 开发了所谓的“C shell”(csh),其灵感至少部分来源于 Mashey 的语法,并引入了显著的增强功能以​​供交互使用,包括历史记录、别名和作业控制。cshtcsh版本添加了命令行编辑和命令完成功能。 David Korn 将这些机制融入了 Bourne shell 的直系后代ksh中,该 shell 非常类似于标准 POSIX shell [ Int03b ]。流行的“Bourne-again”shell bash是ksh的开源版本。虽然tcsh在某些领域仍然很流行,但ksh/bash /POSIX sh在编写 shell 脚本方面要好得多,在交互使用方面也堪称一流。

In the late 1970s Bill Joy developed the so-called “C shell” (csh), inspired at least in part by Mashey's syntax, and introducing significant enhancements for interactive use, including history, aliases, and job control. The tcsh version of csh adds command-line editing and command completion. David Korn incorporated these mechanisms into a direct descendant of the Bourne shell, ksh, which is very similar to the standard POSIX shell [Int03b]. The popular “Bourne-again” shell, bash, is an open-source version of ksh. While tcsh is still popular in some quarters, ksh/bash/POSIX sh is substantially better for writing shell scripts, and comparable for interactive use.

除了专为交互使用而设计的功能(我们在此不再赘述)之外,shell 语言还提供了大量机制来操作文件名、参数和命令,以及将其他程序粘合在一起。大多数这些功能都保留在更通用的脚本语言中。我们在此使用bash语法来考虑其中的一些功能。讨论必然会大大简化;完整详细信息可在bash 手册页或各种在线教程中找到。

In addition to features designed for interactive use, which we will not consider further here, shell languages provide a wealth of mechanisms to manipulate filenames, arguments, and commands, and to glue together other programs. Most of these features are retained by more general scripting languages. We consider a few of them here, using bash syntax. The discussion is of necessity heavily simplified; full details can be found in the bash man page, or in various on-line tutorials.

文件名和变量扩展

Filename and Variable Expansion

例 14.3

Example 14.3

“通配符”和“通配符”

“Wildcards” and “globbing”

大多数 Unix shell 用户都熟悉文件名的“通配符”扩展。以下命令将列出当前目录中名称以.pdf结尾的所有文件:

Most users of a Unix shell are familiar with “wildcard” expansion of file names. The following command will list all files in the current directory whose names end in .pdf:

ls *.pdf

ls *.pdf

shell 将模式*.pdf扩展为所有匹配名称的列表。如果有三个(例如fig1.pdf、fig2.pdffig3.pdf),结果相当于

The shell expands the pattern *.pdf into a list of all matching names. If there are three ofthem (say fig1.pdf, fig2.pdf, and fig3.pdf), the result is equivalent to

ls 图1.pdf 图2.pdf 图3.pdf

ls fig1.pdf fig2.pdf fig3.pdf

文件名扩展有时也称为“通配符”,以实现它的原始 Unix glob命令命名。除了*通配符外,通常还可以指定“无关”或其他字符或子字符串。模式fig?.pdf将匹配(扩展为) g和点之间有一个字符的任何文件。模式fig[0-9].pdf将要求该字符为数字。模式fig3.{eps,pdf}将匹配fig3.epsfig3.pdf。■

Filename expansion is sometimes called “globbing,” after the original Unix glob command that implemented it. In addition to * wildcards, one can usually specify “don't care” or alternative characters or substrings. The pattern fig?.pdf will match (expand to) any file(s) with a single character between the g and the dot. The pattern fig[0-9].pdf will require that character to be a digit. The pattern fig3.{eps,pdf} will match both fig3.eps and fig3.pdf. ■

例 14.4

Example 14.4

shell 中的For循环

For loops in the shell

文件名扩展在循环中特别有用。此类循环可以直接从键盘输入,也可以嵌入到脚本中以供以后执行。例如,假设我们希望创建所有 EPS 图形的 PDF 版本:1

Filename expansion is particularly useful in loops. Such loops may be typed directly from the keyboard, or embedded in scripts intended for later execution. Suppose, for example, that we wish to create PDF versions of all our EPS figures:1

对于 *.eps 格式的图

for fig in *.eps

do

 ps2pdf $图

 ps2pdf $fig

完毕

done

for结构安排 shell 变量fig在循环的连续迭代中逐个采用*.eps扩展中的名称。第 3 行中的美元符号导致fig的值在执行ps2pdf命令之前扩展为该命令。(有趣的是, ps2pdf本身是一个调用gs Postscript 解释器的 shell 脚本。)可选括号可用于将变量名称与后续字符分开,例如cp $foo ${foo}_backup。■

The for construct arranges for the shell variable fig to take on the names in the expansion of *.eps, one at a time, in consecutive iterations of the loop. The dollar sign in line 3 causes the value of fig to be expanded into the ps2pdf command before it is executed. (Interestingly, ps2pdf is itself a shell script that calls the gs Postscript interpreter.) Optional braces can be used to separate a variable name from following characters, as in cp $foo ${foo}_backup. ■

例 14.5

Example 14.5

一行一个循环

A whole loop on one line

如果用分号分隔,则可以在一行中输入多个命令。例如,以下内容相当于上例中的循环:

Multiple commands can be entered on a single line if they are separated by semicolons. The following, for example, is equivalent to the loop in the previous example:

对于 *.eps 中的图;执行 ps2pdf $fig;完成

for fig in *.eps; do ps2pdf $fig; done

测试、查询和条件

Tests, Queries, and Conditions

例 14.6

Example 14.6

shell 中的条件测试

Conditional tests in the shell

上述循环将对当前目录中的每个 EPS 文件执行ps2pdf。但是,假设我们已经有一些 PDF 文件,并且只想创建缺少的文件:

The loop above will execute ps2pdf for every EPS file in the current directory. Suppose, however, that we already have some PDF files, and only want to create the ones that are missing:

对于 *.eps 格式的图

for fig in *.eps

do

 目标=${fig%.eps}.pdf

 target=${fig%.eps}.pdf

 如果[$fig-nt$target]

 if [ $fig -nt $target ]

 然后

 then

  ps2pdf $图

  ps2pdf $fig

 

 fi

完毕

done

此脚本的第三行是变量赋值。右侧的表达式${fig%.eps}扩展为fig的值,并删除了所有尾随的.eps。类似的特殊扩展可用于以多种不同方式测试或修改变量的值。第四行中的方括号界定条件测试。-nt运算符检查其左操作数命名的文件是否比其右操作数命名的文件新(或者左操作数存在但右操作数不存在)。类似的文件查询运算符可用于检查文件的许多其他属性。其他运算符可用于算术或字符串比较。■

The third line of this script is a variable assignment. The expression ${fig%.eps} within the right-hand side expands to the value of fig with any trailing .eps removed. Similar special expansions can be used to test or modify the value of a variable in many different ways. The square brackets in line four delimit a conditional test. The -nt operator checks to see whether the file named by its left operand is newer than the file named by its right operand (or if the left operand exists but the right does not). Similar file query operators can be used to check many other properties of files. Additional operators can be used for arithmetic or string comparisons. ■

设计与实现

Design & Implementation

14.3 shell 中的内置命令

14.3 Built-in commands in the shell

shell 中的命令通常采用单词序列的形式,其中第一个单词是命令的名称。大多数命令都是可执行程序,位于 shell 的搜索路径上的目录中。但是,大量的命令(在bash中大约有 50 个)是内置命令— shell 可以识别并自行执行这些命令,而不是启动外部程序。有趣的是,一些可用作单独程序的命令被复制为内置命令,这要么是为了提高效率,要么是为了提供额外的语义。例如,条件测试最初由外部测试命令(方括号是语法糖)支持,但是这些测试在脚本中出现的频率足够高,因此在添加内置版本后,执行速度显著提高。相比之下,虽然kill命令不经常使用,但内置版本允许通过 shell 的作业控制机制中的小整数或符号名称来识别进程。外部版本仅支持操作系统提供的较长且相对不直观的进程标识符。

Commands in the shell generally take the form of a sequence of words, the first of which is the name of the command. Most commands are executable programs, found in directories on the shell's search path. A large number, however (about 50 in bash), are builtins—commands that the shell recognizes and executes itself, rather than starting an external program. Interestingly, several commands that are available as separate programs are duplicated as builtins, either for the sake of efficiency or to provide additional semantics. Conditional tests, for example, were originally supported by the external test command (for which square brackets are syntactic sugar), but these occur sufficiently often in scripts that execution speed improved significantly when a built-in version was added. By contrast, while the kill command is not used very often, the built-in version allows processes to be identified by small integer or symbolic names from the shell's job control mechanism. The external version supports only the longer and comparatively unintuitive process identifiers supplied by the operating system.

管道和重定向

Pipes and Redirection

例 14.7

Example 14.7

管道

Pipes

Unix 的主要创新之一是能够将命令链接在一起,将一个命令的输出“管道化”为下一个命令的输入。与大多数 shell 一样,bash使用竖线字符 ( | ) 来表示管道。要计算目录中的数字数量(不区分 EPS 和 PDF 版本),我们可以输入

One of the principal innovations of Unix was the ability to chain commands together, “piping” the output of one to the input of the next. Like most shells, bash uses the vertical bar character (|) to indicate a pipe. To count the number of figures in our directory, without distinguishing between EPS and PDF versions, we might type

对于 * 中的图;执行 echo ${fig/.*};完成 | sort -u | wc -l

for fig in *; do echo ${fig/.*}; done | sort -u | wc -l

这里第一个命令是for循环,它打印所有删除了扩展名(点后缀)的文件的名称。循环内的echo命令只打印其参数。循环后的sort -u命令删除重复项, wc -l命令计算行数。■

Here the first command, a for loop, prints the names of all files with extensions (dot-suffixes) removed. The echo command inside the loop simply prints its arguments. The sort -u command after the loop removes duplicates, and the wc -l command counts lines. ■

例 14.8

Example 14.8

输出重定向

Output redirection

与大多数 shell 一样,bash还允许将输出重定向到文件,或从文件读取输入。要创建数字列表,我们可以输入

Like most shells, bash also allows output to be redirected to a file, or input read from a file. To create a list offigures, we might type

对于 * 中的图;执行 echo ${fig/.*};完成 | sort -u > all_figs

for fig in *; do echo ${fig/.*}; done | sort -u > all_figs

“大于”符号表示输出重定向。如果将其加倍(sort -u >> all_figs),则会导致将输出附加到指定文件,而不是覆盖先前的内容。

The “greater than” sign indicates output redirection. If doubled (sort -u >> all_figs) it causes output to be appended to the specified file, rather than overwriting the previous contents.

类似地,“小于”符号表示输入重定向。假设我们想将数字列表全部打印在一行上,用空格分隔,而不是打印在多行上。在 Unix 系统上,我们可以输入

In a similar vein, the “less than” sign indicates input redirection. Suppose we want to print our list of figures all on one line, separated by spaces, instead of on multiple lines. On a Unix system we can type

tr'\n'''<all_figs

tr '\n' ' ' < all_figs

标准tr命令的调用将所有换行符转换为空格。由于tr是作为简单过滤器编写的,因此它不接受命令行上的文件列表;它只读取标准输入。■

This invocation of the standard tr command converts all newline characters to spaces. Because tr was written as a simple filter, it does not accept a list of files on the command line; it only reads standard input. ■

例 14.9

Example 14.9

stderrstdout的重定向

Redirection of stderr and stdout

对于任何正在执行的 Unix 程序,操作系统都会跟踪打开的文件列表。按照惯例,标准输入标准输出stdinstdout)是文件编号 0 和 1。文件编号 2 按照惯例是标准错误stderr),程序应该将诊断错误消息打印到该文件中。sh系列shell 相对于csh系列的优势之一是能够独立于stdin和stdout 重定向stderr和其他打开的文件例如,考虑ps2pdf脚本。在正常情况下,此脚本会默默运行。但是,如果遇到错误,它会将消息打印到stdout并退出。当从键盘调用命令时,这种违反惯例(消息应该发送到stderr)是无害的。但是,如果它嵌入在脚本中,并且脚本的输出定向到文件,则错误消息可能会出现在文件中而不是屏幕上,并且用户不会注意到。使用bash,我们可以输入

For any executing Unix program, the operating system keeps track of a list of open files. By convention, standard input and standard output (stdin and stdout) are files numbers 0 and 1. File number 2 is by convention standard error (stderr), to which programs are supposed to print diagnostic error messages. One of the advantages of the sh family of shells over the csh family is the ability to redirect stderr and other open files independent of stdin and stdout. Consider, for example, the ps2pdf script. Under normal circumstances this script works silently. If it encounters an error, however, it prints a message to stdout and quits. This violation of convention (the message should go to stderr) is harmless when the command is invoked from the keyboard. If it is embedded in a script, however, and the output of the script is directed to a file, the error message may end up in the file instead of on the screen, and go unnoticed by the user. With bash we can type

ps2pdf my_fig.eps 1>&2

ps2pdf my_fig.eps 1>&2

这里1>&2的意思是“让ps2pdf将文件 1 ( stdout ) 发送到周围上下文通常发送文件 2 ( stderr ) 的同一位置。”■

Here 1>&2 means “make ps2pdf send file 1 (stdout) to the same place that the surrounding context would normally send file 2 (stderr).” ■

例 14.10

Example 14.10

Heredocs(内联输入)

Heredocs (in-line input)

最后,与大多数 shell 一样,bash允许用户以内联方式提供命令输入:

Finally, like most shells, bash allows the user to provide the input to a command in-line:

tr'\n'''<<结束

tr '\n' ' ' <<END

列表

list

of

输入

input

线

lines

结尾

END

<<END表示后续输入行(直到只包含END 的行)将作为tr的输入提供。此类内联输入(传统上称为“此处文档”)很少以交互方式使用,但在 shell 脚本中非常有用。■

The <<END indicates that subsequent input lines, up to a line containing only END, are to be supplied as input to tr. Such in-line input (traditionally called a “here document”) is seldom used interactively, but is highly useful in shell scripts. ■

引用与扩展

Quoting and Expansion

例 14.11

Example 14.11

文件名中存在空格问题

Problematic spaces in file names

前面小节中的几个示例都隐含地依赖于文件名不包含空格的假设。回到示例 14.4,如果我们尝试在包含名为two words.eps 的文件的目录中运行循环,我们将遇到“文件未找到”错误:ps2pdf最终会将其参数解释为两个单词而不是一个,并尝试将文件two(不存在)转换为words.eps。为了避免此类问题,shell 通常提供一种引用机制,将单词组合成字符串。我们可以通过键入以下内容来修复示例 14.4

Several examples in the preceding subsections have implicitly relied on the assumption that file names do not contain spaces. Returning to Example 14.4, we will encounter a “file not found” error if we try to run our loop in a directory that contains a file named two words.eps: ps2pdf will end up interpreting its arguments as two words instead of one, and will try to translate file two (which doesn't exist) into words.eps. To avoid problems like this, shells typically provide a quoting mechanism that will group words together into strings. We could fix Example 14.4 by typing

对于 *.eps 格式的图

for fig in *.eps

do

 ps2pdf“$fig”

 ps2pdf “$fig”

完毕

done

这里, $fig两侧的双引号使其被解释为一个单词,即使它包含空格。■

Here the double quotes around $fig cause it to be interpreted as a single word, even if it contains white space. ■

例 14.12

Example 14.12

单引号和双引号

Single and double quotes

但这不是唯一的引用方式。单引号(直引号或正引号)也将文本分组为单词,但会抑制引用文本中的文件名和变量扩展。因此

But this is not the only kind of quoting. Single (straight or forward) quotes also group text into words, but inhibit filename and variable expansion in the quoted text. Thus

foo=bar

foo=bar

单='$foo'

single='$foo'

double=“$foo”

double=“$foo”

回显 $single $double

echo $single $double

将打印“ $foo bar ”。■

will print “$foo bar“. ■

例 14.13

Example 14.13

子壳层

Subshells

bash中还有其他几个括号结构将文本分组到其中,用于各种目的。括在括号中的命令列表将传递给子 shell 进行评估。如果左括号前面有一个美元符号,则嵌套命令列表的输出将扩展到周围的上下文中:

Several other bracketing constructs in bash group the text inside, for various purposes. Command lists enclosed in parentheses are passed to a subshell for evaluation. If the opening parenthesis is preceded by a dollar sign, the output of the nested command list is expanded into the surrounding context:

对于 $(cat my_figs) 中的图;执行 ps2pdf ${fig}.eps;完成

for fig in $(cat my_figs); do ps2pdf ${fig}.eps; done

这里cat是打印文件内容的标准命令。大多数 shell 使用向后的单引号来实现相同的目的('cat my_figs');bash也支持此语法,以实现向后兼容。■

Here cat is the standard command to print the content of a file. Most shells use backward single quotes for the same purpose ('cat my_figs'); bash supports this syntax as well, for backward compatibility. ■

例 14.14

Example 14.14

shell 中括号引用的块

Brace-quoted blocks in the shell

括号内的命令列表被bash视为一个单元。例如,它们可用于重定向命令序列的输出:

Command lists enclosed in braces are treated by bash as a single unit. They can be used, for example, to redirect the output of a sequence of commands:

{ 日期; ls; } >> 文件列表

{ date; ls; } >> file_list

与带括号的列表不同,括号中的命令由当前 shell 执行。从编程语言的角度来看,括号和括号的行为与 C 中的行为“相反”:括号在bash 中引入了嵌套的动态范围,而括号仅用于分组。具体而言,在带括号的命令列表中被赋予新值的变量将在列表执行完成后恢复为先前的值。■

Unlike parenthesized lists, commands enclosed in braces are executed by the current shell. From a programming languages perspective, parentheses and braces behave “backward” from the way they do in C: parentheses introduce a nested dynamic scope in bash, while braces are purely for grouping. In particular, variables that are assigned new values within a parenthesized command list will revert to their previous values once the list has completed execution. ■

例 14.15

Example 14.15

基于模式的列表生成

Pattern-based list generation

当括号周围没有空格时,括号会执行基于模式的列表生成,其方式类似于文件名扩展,但不连接到文件系统。例如,echo abc{12,34,56}xyz会打印abc12xyz abc34xyz abc56xyz。此外,正如我们所见,当左括号前面有美元符号时,括号用于分隔变量名称。■

When not surrounded by white space, braces perform pattern-based list generation, in a manner similar to filename expansion, but without the connection to the file system. For example, echo abc{12,34,56}xyz prints abc12xyz abc34xyz abc56xyz. Also, as we have seen, braces serve to delimit variable names when the opening brace is preceded by a dollar sign. ■

示例 14.6中,我们使用方括号括起条件表达式。双方括号具有类似的用途,但表达式语法更像 C,并且没有文件名扩展。双括号用于括起算术计算,同样具有类似 C 的语法。

In Example 14.6 we used square brackets to enclose a conditional expression. Double square brackets serve a similar purpose, but with more C-like expression syntax, and without filename expansion. Double parentheses are used to enclose arithmetic computations, again with C-like syntax.

在$()或反引号中插入命令、在{}中插入模式以及在(())中插入算术表达式都被视为扩展形式,类似于文件名扩展和变量扩展。将字符串拆分为单词也被视为一种扩展形式,在某些情况下,用用户主目录的名称替换波浪符号 ( ~ ) 也被视为一种扩展形式。总而言之,这些为我们提供了Bash 中的七种不同类型的扩展。

The interpolation of commands in $() or backquotes, patterns in {}, and arithmetic expressions in (()) are all considered forms of expansion, analogous to filename expansion and variable expansion. The splitting of strings into words is also considered a form of expansion, as is the replacement, in certain contexts, of tilde (~) characters with the name of the user's home directory. All told, these give us seven different kinds of expansion in bash.

所有各种括号结构都有规则来规定在其中执行哪些类型的扩展。这些规则旨在尽可能直观,但它们在各个结构中并不统一。例如,文件名扩展不会在[[ ]]括号条件中发生。类似地,如果使用反斜杠转义,双引号字符可能会出现在双引号字符串内,但单引号字符可能不会出现在单引号字符串内。

All of the various bracketing constructs have rules governing which kinds of expansion are performed within. The rules are intended to be as intuitive as possible, but they are not uniform across constructs. Filename expansion, for example, does not occur within [[ ]] -bracketed conditions. Similarly, a double-quote character may appear inside a double-quoted string if escaped with a backslash, but a single-quote character may not appear inside a single-quoted string.

功能

Functions

例 14.16

Example 14.16

用户定义的 shell 函数

User-defined shell functions

用户可以在bash中定义函数,然后像内置命令一样工作。例如,许多用户将ll定义为ls -l的快捷方式,它以“长格式”列出当前目录中的文件:

Users can define functions in bash that then work like built-in commands. Many users, for example, define ll as a shortcut for ls -l, which lists files in the current directory in “long format”:

函数 ll () {

function ll () {

 ls -l “$@”

 ls -l “$@”

}

}

在函数中,$1表示第一个参数,$2表示第二个参数,依此类推。在ll的定义中,$@表示整个参数列表。函数可以任意复杂。特别是,bash支持局部变量和递归。csh系列中的 Shell提供了一种更原始​​的别名机制,该机制通过宏扩展来实现。■

Within the function, $1 represents the first parameter, $2 represents the second, and so on. In the definition of ll, $@ represents the entire parameter list. Functions can be arbitrarily complex. In particular, bash supports both local variables and recursion. Shells in the csh family provide a more primitive alias mechanism that works via macro expansion. ■

#! 约定

The #! Convention

例 14.17

Example 14.17

脚本文件中的# !约定

The #! convention in script files

如上所述,shell 命令可以从脚本文件中读取。要在当前 shell 中执行它们,请使用“dot”命令:

As noted above, shell commands can be read from a script file. To execute them in the current shell, one uses the “dot” command:

.我的脚本

. my_script

其中my_script是文件的名称。许多操作系统(包括大多数版本的 Unix)允许将脚本文件转换为可执行程序,以便用户只需键入

where my_script is the name of the file. Many operating systems, including most versions of Unix, allow one to turn a script file into an executable program, so that users can simply type

我的脚本

my_script

需要两个步骤。首先,文件必须在操作系统眼中标记为可执行。在 Unix 上,键入chmod +x my_script。其次,文件必须具有自描述性,以便操作系统能够判断哪个 shell(或其他解释器)将理解其内容。在 Unix 下,文件必须以字符#!开头,后跟 shell 的名称。因此,典型的bash脚本以

Two steps are required. First, the file must be marked executable in the eyes of the operating system. On Unix one types chmod +x my_script. Second, the file must be self-descriptive in a way that allows the operating system to tell which shell (or other interpreter) will understand the contents. Under Unix, the file must begin with the characters #!, followed by the name of the shell. The typical bash script thus begins with

/bin/bash #!/bin/bash

#!/bin/bash

指定完整路径名是一种安全功能:它预计用户可能有一个搜索路径,其中某个名为bash的其他程序出现在 shell 之前。(不幸的是,对完整路径名的要求使#!行不可移植,因为 shell 和其他解释器可能安装在不同机器的不同位置。)■

Specifying the full path name is a safety feature: it anticipates the possibility that the user may have a search path for commands on which some other program named bash appears before the shell. (Unfortunately, the requirement for full path names makes #! lines nonportable, since shells and other interpreters may be installed in different places on different machines.) ■

14-01-9780124104099检查你的理解

Check Your Understanding

1. 请用一句话给出“脚本语言”的合理定义。

1. Give a plausible one-sentence definition of “scripting language.”

2. 列出脚本语言与传统“系统”语言的主要区别。

2. List the principal ways in which scripting languages differ from conventional “systems” languages.

3. 现代脚本语言源自哪两组主要祖先?

3. From what two principal sets of ancestors are modern scripting languages descended?

4.  IBM 的哪项发明被普遍认为是第一个通用脚本语言?

4. What IBM creation is generally considered the first general-purpose scripting language?

5. 最流行的服务器端 Web 脚本语言是什么?

5. What is the most popular language for server-side web scripting?

6. Perl 中的上下文概念与强制有何不同?

6. How does the notion of context in Perl differ from coercion?

7. 什么是通配符?什么是通配符

7. What is globbing? What is a wildcard?

8. Unix 中的 管道是什么?什么是重定向

8. What is a pipe in Unix? What is redirection?

9. 描述为每个 Unix 进程提供的三个标准 I/O 流。

9. Describe the three standard I/O streams provided to every Unix process.

10. 解释 Unix shell 脚本中 #! 约定的意义。

10. Explain the significance of the #! convention in Unix shell scripts.

设计与实现

Design & Implementation

14.4 魔法数字

14.4 Magic numbers

当 Unix 内核被要求执行一个文件时(通过execve系统调用),它会检查文件的前几个字节,寻找指示文件类型的“魔数”。一些值对应于直接可执行的目标文件格式。例如,在 Linux 下,目标文件的前四个字节是0x7f45_4c46(ASCII 中的 〈del〉ELF)。在 Mac OS X 下,它们是0xfeed_face。如果前两个字节是0x2321( ASCII 中的#!),内核会假定该文件是一个脚本,并读取后续字符以查找解释器的名称。

When the Unix kernel is asked to execute a file (via the execve system call), it checks the first few bytes of the file for a “magic number” that indicates the file's type. Some values correspond to directly executable object file formats. Under Linux, for example, the first four bytes of an object file are 0x7f45_4c46 (〈del〉ELF in ASCII). Under Mac OS X they are 0xfeed_face. If the first two bytes are 0x2321 (#! in ASCII), the kernel assumes that the file is a script, and reads subsequent characters to find the name of the interpreter.

Unix 中的# !约定是大多数脚本语言使用#作为开始注释分隔符的主要原因。sh早期版本使用无操作命令 (:) 作为引入注释的方式。Joy 的 C shell 引入了#,因此sh的某些版本经过修改,当被要求执行似乎以 C shell 注释开头的脚本时会启动csh。这种机制演变为当今许多(但不是全部)Unix 变体中使用的更通用的机制。

The #! convention in Unix is the main reason that most scripting languages use # as the opening comment delimiter. Early versions of sh used the no-op command (:) as a way to introduce comments. Joy's C shell introduced #, whereupon some versions of sh were modified to launch csh when asked to execute a script that appeared to begin with a C shell comment. This mechanism evolved into the more general mechanism used in many (though not all) variants of Unix today.

14.2.2 文本处理和报告生成

14.2.2 Text Processing and Report Generation

Shell 语言往往高度面向字符串。命令是字符串,解析为单词列表。变量是字符串值。变量扩展机制允许用户提取前缀、后缀或任意子字符串。连接通过简单的并置表示。有复杂的引用约定。很少有更传统的语言对字符串有类似的支持。

Shell languages tend to be heavily string-oriented. Commands are strings, parsed into lists of words. Variables are string-valued. Variable expansion mechanisms allow the user to extract prefixes, suffixes, or arbitrary substrings. Concatenation is indicated by simple juxtaposition. There are elaborate quoting conventions. Few more conventional languages have similar support for strings.

同时,shell 语言显然不适用于emacsvim 等编辑器中常见的文本操作。特别是,shell 语言缺少搜索和替换功能,而且编辑器通过一次击键即可完成的许多其他任务(插入、删除、替换、括号匹配、向前和向后移动)在 shell 环境中很难实现,或者根本没有意义。对于重复的文本操作,人们自然希望使编辑过程自动化。完成此任务的工具构成了现代脚本语言的第二大祖先类。

At the same time, shell languages are clearly not intended for the sort of text manipulation commonly performed in editors like emacs or vim. Search and substitution, in particular, are missing, and many other tasks that editors accomplish with a single keystroke—insertion, deletion, replacement, bracket matching, forward and backward motion—would be awkward to implement, or simply make no sense, in the context of the shell. For repetitive text manipulation it is natural to want to automate the editing process. Tools to accomplish this task constitute the second principal class of ancestors for modern scripting languages.

塞德

Sed

例 14.18

Example 14.18

使用sed提取 HTML 标头

Extracting HTML headers with sed

举一个简单的文本处理示例,考虑从网页(HTML 文件)中提取所有标题的问题。这些是用<h1> … </h1><h2> … </h2><h3> … </h3>标签分隔的字符串。在emacsvim甚至 Microsoft Word等编辑器中完成这项任务很简单但繁琐:必须搜索开始标记、删除前面的文本、搜索结束标记、标记当前位置(作为下一次删除的起点),然后重复。图 14.1显示了在 Unix“流编辑器” sed中执行这些任务的程序。代码由一个标签和三个命令组成,其中前两个是复合命令。第一个复合命令打印在当前正在检查的输入部分(sed称之为模式空间)中找到的第一个标题(如果有)。第二个复合命令只要模式空间已经包含标题开始标记,就会将新行附加到模式空间。这两个复合命令和几个子命令都使用正则表达式模式,以斜线分隔。我们将在14.4.2 节中进一步讨论这些模式。第三个命令(单独的d)只是删除模式空间。由于每个复合命令都以返回脚本顶部的分支结尾,因此只有当第一个命令不执行时,第二个命令才会执行,而只有当两个复合命令都不执行时,删除命令才会执行。■

As a simple text processing example, consider the problem of extracting all headers from a web page (an HTML file). These are strings delimited by <h1> … </h1>, <h2> … </h2>, and <h3> … </h3> tags. Accomplishing this task in an editor like emacs, vim, or even Microsoft Word is straightforward but tedious: one must search for an opening tag, delete preceding text, search for a closing tag, mark the current position (as the starting point for the next deletion), and repeat. A program to perform these tasks in sed, the Unix “stream editor,” appears in Figure 14.1. The code consists of a label and three commands, the first two of which are compound. The first compound command prints the first header, if any, found in the portion of the input currently being examined (what sed calls the pattern space). The second compound command appends a new line to the pattern space whenever it already contains a header-opening tag. Both compound commands, and several of the subcommands, use regular expression patterns, delimited by slashes. We will discuss these patterns further in Section 14.4.2. The third command (the lone d) simply deletes the pattern space. Because each compound command ends with a branch back to the top of the script, the second will execute only if the first does not, and the delete will execute only if neither compound does. ■

f14-01-9780124104099
图 14.1 sed 中的脚本用于从 HTML 文件中提取标题。该脚本假定开始和结束标记正确匹配,并且标题不嵌套。

例 14.19

Example 14.19

sed中的单行脚本

One-line scripts in sed

在此示例中, sed的编辑器传统显而易见。命令通常只有一个字符长,并且没有变量 — 除了程序计数器和正在编辑的文本之外,没有任何类型的状态。这些限制使得sed最适合“单行程序”,通常使用-e命令行开关从键盘逐字输入。例如,下面的命令将从标准输入读取、删除空行并(隐式地)将非空行打印到标准输出:

The editor heritage of sed is clear in this example. Commands are generally one character long, and there are no variables—no state of any kind beyond the program counter and text that is being edited. These limitations make sed best suited to “one-line programs,” typically entered verbatim from the keyboard with the -e command-line switch. The following, for example, will read from standard input, delete blank lines, and (implicitly) print the nonblank lines to standard output:

sed -e'/^[[:space:]]*$/d'

sed -e'/^[[:space:]]*$/d'

这里 ^ 表示行首,$表示行尾。[[:space:]]表达式匹配本地字符集中的任何空白字符,可重复任意次数,如 Kleene 星号 ( * ) 所示。d表示删除。默认情况下会打印未删除的行。■

Here ^ represents the beginning of the line and $ represents the end. The [[:space:]] expression matches any white-space character in the local character set, to be repeated an arbitrary number of times, as indicated by the Kleene star (*). The d indicates deletion. Nondeleted lines are printed by default. ■

awk 的

Awk

为了解决sed的局限性,Alfred Aho、Peter Weinberger 和 Brian Kernighan 于 1977 年设计了awk(该名称取自他们姓氏的首字母)。从某种意义上说,Awk是sed等流编辑器与成熟脚本语言之间的进化纽带。它保留了sed的一次一行过滤计算模型,但允许用户在需要时退出该模型,并使用类似于 C 的语法替换单字符编辑命令。Awk提供(无类型)变量和各种控制流构造,包括子例程。

In an attempt to address the limitations of sed, Alfred Aho, Peter Weinberger, and Brian Kernighan designed awk in 1977 (the name is based on the initial letters of their last names). Awk is in some sense an evolutionary link between stream editors like sed and full-fledged scripting languages. It retains sed's line-at-a-time filter model of computation, but allows the user to escape this model when desired, and replaces single-character editing commands with syntax reminiscent of C. Awk provides (typeless) variables and a variety of control-flow constructs, including subroutines.

例 14.20

Example 14.20

使用awk提取 HTML 标头

Extracting HTML headers with awk

awk程序由一系列模式组成,每个模式都有一个与之关联的动作。对于每一行输入,解释器按顺序执行模式求值为真的动作。图 14.2 显示了具有单个模式-动作对的示例它执行的任务与图 14.1中的sed脚本基本相同。不包含开始标记的行将被忽略。在带有开始标记的行中,我们删除标题之前的所有文本。然后我们打印各行直到找到结束标记,如果同一行上有另一个开始标记,则重复此操作。当我们完全超出任何标题范围时,我们将回到解释器的主循环。

An awk program consists of a sequence of patterns, each of which has an associated action. For every line of input, the interpreter executes, in order, the actions whose patterns evaluate to true. An example with a single pattern-action pair appears in Figure 14.2. It performs essentially the same task as the sed script of Figure 14.1. Lines that contain no opening tag are ignored. In a line with an opening tag, we delete any text that precedes the header. We then print lines until we find the closing tag, and repeat if there is another opening tag on the same line. We fall back into the interpreter's main loop when we're cleanly outside any header.

f14-02-9780124104099
图 14.2 awk 中的脚本用于从 HTML 文件中提取标题。与 sed 脚本不同,此版本以增量方式打印内部行。它再次假定输入格式正确。

从这个例子中可以看出几个惯例。当前输入行在伪变量$0 中可用。默认情况下,getline函数会读入此变量。substr (s, a, b)函数提取字符串s中从位置a开始、长度为b的部分如果省略b,则提取的部分会一直到 s 的末尾条件(如模式)可以使用正则表达式;我们可以在dowhile循环中看到一个例子。默认情况下,正则表达式与$0匹配。■

Several conventions can be seen in this example. The current input line is available in the pseudovariable $0. The getline function reads into this variable by default. The substr(s, a, b) function extracts the portion of string s starting at position a and with length b. If b is omitted, the extracted portion runs to the end of s. Conditions, like patterns, can use regular expressions; we can see an example in the dowhile loop. By default, regular expressions match against $0. ■

例 14.21

Example 14.21

awk中的字段

Fields in awk

awk最重要的两个创新可能是字段关联数组,但这两者都未出现在图 14.2中。与 shell 类似,awk将每个输入行解析为一系列单词(字段)。默认情况下,这些单词由空格分隔,但用户可以通过将正则表达式分配给内置变量FS(字段分隔符)来动态更改此行为。当前输入行的字段在伪变量$1、$2、...中可用。内置变量NR给出字段的总数。awk 经常用于基于字段的单行程序。例如,下面的代码将打印标准输入每一行的第二个单词:

Perhaps the two most important innovations of awk are fields and associative arrays, neither of which appears in Figure 14.2. Like the shell, awk parses each input line into a series of words (fields). By default these are delimited by white space, though the user can change this behavior dynamically by assigning a regular expression to the built-in variable FS (field separator). The fields of the current input line are available in the pseudovariables $1, $2, …. The built-in variable NR gives the total number of fields. Awk is frequently used for field-based one-line programs. The following, for example, will print the second word of every line of standard input:

awk'{打印$2}'

awk '{print $2}'

例 14.22

Example 14.22

在awk中将标题大写

Capitalizing a title in awk

关联数组将在14.4.3 节中详细讨论。简而言之,它们将哈希表的功能与数组的语法结合在一起。我们可以通过一个示例脚本(图 14.3)来说明字段和关联数组,该脚本将其输入的每一行都大写,就好像它是一个标题一样。该脚本拒绝修改“噪声”词(冠词、连词和短介词),除非它们是标题或副标题的第一个词,其中副标题跟在以冒号或破折号结尾的单词后面。该脚本还拒绝修改除第一个字母以外的任何字母已经大写的单词。■

Associative arrays will be considered in more detail in Section 14.4.3. Briefly, they combine the functionality of hash tables with the syntax of arrays. We can illustrate both fields and associative arrays with an example script (Figure 14.3) that capitalizes each line of its input as if it were a title. The script declines to modify “noise” words (articles, conjunctions, and short prepositions) unless they are the first word of the title or of a subtitle, where a subtitle follows a word ending with a colon or a dash. The script also declines to modify words in which any letter other than the first is already capitalized. ■

f14-03-9780124104099
图 14.3 awk 中的脚本将标题大写。BEGIN块在读取任何输入行之前执行。主块没有明确的模式因此它应用于每个输入行。

Perl

Perl

Perl 最初由 Larry Wall 于 1987 年开发,当时他在美国国家安全局工作。最初的版本,大致是试图结合sed、awksh 的最佳功能。它是一个仅适用于 Unix 的工具,主要用于文本处理(名称代表“实用提取和报告语言”)。多年来,Perl 发展成为一种庞大而复杂的语言,拥有庞大的用户社区。多年来,它无疑是最流行和使用最广泛的脚本语言,尽管这一领先地位最近已被 Python、Ruby 和其他语言所取代。Perl 的速度足以满足许多通用用途,并且包括适合大型项目的单独编译、模块化和动态库机制。它已被移植到几乎所有已知的操作系统。

Perl was originally developed by Larry Wall in 1987, while he was working at the National Security Agency. The original version was, to first approximation, an attempt to combine the best features of sed, awk, and sh. It was a Unix-only tool, meant primarily for text processing (the name stands for “practical extraction and report language”). Over the years Perl grew into a large and complex language, with an enormous user community. For many years it was clearly the most popular and widely used scripting language, though that lead has more recently been lost to Python, Ruby, and others. Perl is fast enough for much general-purpose use, and includes separate compilation, modularization, and dynamic library mechanisms appropriate for large-scale projects. It has been ported to almost every known operating system.

Perl 的语言核心相对简单,但有大量内置库函数以及同样多的快捷方式和特殊情况。标准语言参考 [ CfWO12,第 722 页] 中可以找到这种丰富表达方式的提示,其中仅列出了 97 个“行为在不同平台之间差异最大的”内置函数。第三版的封面上印着这样一句话:“做事不止一种方法。”

Perl consists of a relatively simple language core, augmented with an enormous number of built-in library functions and an equally enormous number of shortcuts and special cases. A hint at this richness of expression can be found in the standard language reference [CfWO12, p. 722], which lists (only) the 97 built-in functions “whose behavior varies the most across platforms.” The cover of the third edition was emblazoned with the motto: “There's more than one way to do it.”

例 14.23

Example 14.23

使用 Perl 提取 HTML 标头

Extracting HTML headers with Perl

在本章中,我们将多次回到 Perl,特别是在第 14.2.414.4节。目前,我们仅介绍一个简单的文本处理示例,同样是从 HTML 文件中提取标题(图 14.4)。我们可以在该图中看到几个 Perl 快捷方式,其中大多数有助于使代码比sed图 14.1)和awk图 14.2)中的等效程序更短。尖括号(<>)是“readline”运算符,用于文本文件输入。通常它们括住文件句柄变量名,但作为特殊情况,空尖括号会生成脚本首次调用时在命令行上指定的所有文件的串联作为输入(如果没有这样的文件,则生成标准输入)。当 readline 运算符单独出现在while循环的控制表达式中(但语言中其他地方没有出现)时,它会将其输入一次一行地生成到伪变量$_中。默认情况下,其他几个运算符也对$_起作用。例如,正则表达式可用于在任意字符串内搜索,但如果未指定,则假定为$_ 。

We will return to Perl several times in this chapter, notably in Sections 14.2.4 and 14.4. For the moment we content ourselves with a simple text-processing example, again to extract headers from an HTML file (Figure 14.4). We can see several Perl shortcuts in this figure, most of which help to make the code shorter than the equivalent programs in sed (Figure 14.1) and awk (Figure 14.2). Angle brackets (<>) are the “readline” operator, used for text file input. Normally they surround a file handle variable name, but as a special case, empty angle brackets generate as input the concatenation of all files specified on the command line when the script was first invoked (or standard input, if there were no such files). When a readline operator appears by itself in the control expression of a while loop (but nowhere else in the language), it generates its input a line at a time into the pseudovariable $_. Several other operators work on $_ by default. Regular expressions, for example, can be used to search within arbitrary strings, but when none is specified, $_ is assumed.

f14-04-9780124104099
图 14.4 Perl 脚本用于从 HTML 文件中提取标题。为简单起见,我们再次采用了缓冲整个标题的策略,而不是逐步打印它们

next语句类似于C 或 Fortran 中的continue:它跳转到最内层循环的底部并开始下一次迭代。redo语句也会跳过当前迭代的剩余部分,但返回到循环的顶部,而不重新评估控制表达式。在我们的示例程序中,redo允许我们将其他输入附加到当前行,而不是读取新行。由于文件结束通常是通过<>的未定义返回值检测到的,并且由于该失败在每个文件中只会发生一次,因此我们必须在此处使用redo时明确测试eof。请注意,if及其对称对立面,unless,可以用作前缀或后缀测试。

The next statement is similar to continue in C or Fortran: it jumps to the bottom of the innermost loop and begins the next iteration. The redo statement also skips the remainder of the current iteration, but returns to the top of the loop, without reevaluating the control expression. In our example program, redo allows us to append additional input to the current line, rather than reading a new line. Because end-of-file is normally detected by an undefined return value from <>, and because that failure will happen only once per file, we must explicitly test for eof when using redo here. Note that if and its symmetric opposite, unless, can be used as either a prefix or a postfix test.

熟悉 Perl 的读者可能已经注意到脚本第 4 行替换命令中的两个微妙但关键的创新。首先,表达式.*(在sedawk和 Perl 中)匹配允许后续部分匹配成功的最长字符串,而 Perl 中的表达式.*?匹配最短的此类字符串。这种区别使我们能够轻松地隔离给定行中的第一个标题。其次,就像sed允许正则表达式的后续部分引用前面的括号部分(图 14.1的第 4 行)一样,Perl 允许在正则表达式之外使用此类捕获的字符串。我们利用此功能在图 14.4的第 6 行中打印匹配的标题。通常,Perl 的正则表达式比sedawk的正则表达式强大得多;我们将在第 14.4.2 节中更详细地讨论这个主题。■

Readers familiar with Perl may have noticed two subtle but key innovations in the substitution command of line 4 of the script. First, where the expression .* (in sed, awk, and Perl) matches the longest possible string of characters that permits subsequent portions of the match to succeed, the expression .*? in Perl matches the shortest possible such string. This distinction allows us to easily isolate the first header in a given line. Second, much as sed allows later portions of a regular expression to refer back to earlier, parenthesized portions (line 4 of Figure 14.1), Perl allows such captured strings to be used outside the regular expression. We have leveraged this feature to print matched headers in line 6 of Figure 14.4. In general, the regular expressions of Perl are significantly more powerful than those of sed and awk; we will return to this subject in more detail in Section 14.4.2. ■

14.2.3 数学与统计学

14.2.3 Mathematics and Statistics

正如我们在讨论sedawk 时所指出的,文本处理和报告生成的一个显著特征是频繁使用“单行程序”和其他简单脚本。任何曾经在电子表格单元格中输入过公式的人都知道,数学和统计学中也有类似的需求。正如 shell 和报告生成工具已经发展成为用于通用计算的强大语言一样,用于数学和统计计算的符号和工具也已经发展成为强大的语言。

As we noted in our discussions of sed and awk, one of the distinguishing characteristics of text processing and report generation is the frequent use of “one-line programs” and other simple scripts. Anyone who has ever entered formulas in the cells of a spreadsheet realizes that similar needs arise in mathematics and statistics. And just as shell and report generation tools have evolved into powerful languages for general-purpose computing, so too have notations and tools for mathematical and statistical computing.

8.2.1 节(“切片和数组操作”)中,我们提到了 APL,这是 20 世纪 60 年代最不寻常的语言之一。APL 最初被认为是一种用于教授应用数学的纸笔符号,当它演变为一种编程语言时,它仍然强调数学算法的简洁、优雅的表达。虽然它既不能轻松访问其他程序,也不能进行复杂的字符串操作,但 APL 表现出了14.1.1 节中描述的所有其他脚本特征,有时人们会发现它被列为一种脚本语言。

In Section 8.2.1 (“Slices and Array Operations”), we mentioned APL, one of the more unusual languages of the 1960s. Originally conceived as a pen-and-paper notation for teaching applied mathematics, APL retained its emphasis on the concise, elegant expression of mathematical algorithms when it evolved into a programming language. Though it lacked both easy access to other programs and sophisticated string manipulation, APL displayed all the other characteristics of scripting described in Section 14.1.1, and one sometimes finds it listed as a scripting language.

APL 的现代继任者包括三个用于数学计算的商业软件包:Maple、Mathematica 和 Matlab。虽然它们的设计理念不同,但每个都为数值方法、符号数学(公式操作)、数据可视化和数学提供了广泛的支持建模。这三者都提供了强大的脚本语言,并且主要面向科学和工程应用。

The modern successors to APL include a trio of commercial packages for mathematical computing: Maple, Mathematica, and Matlab. Though their design philosophies differ, each provides extensive support for numerical methods, symbolic mathematics (formula manipulation), data visualization, and mathematical modeling. All three provide powerful scripting languages, with a heavy orientation toward scientific and engineering applications.

正如“3M”之于数学计算,S 和 R 语言之于统计计算。S 最初由 John Chambers 及其同事于 20 世纪 70 年代末在贝尔实验室开发,是一款商业软件包,广泛应用于统计学界和社会和行为科学的定量分支。R 是 S 的开源替代品,虽然不完全兼容,但在很大程度上兼容其商业版本。除其他功能外,R 还支持多维数组和列表类型、数组切片操作、用户定义的中缀运算符、按需调用参数、一流函数和无限范围。

As the “3 Ms” are to mathematical computing, so the S and R languages are to statistical computing. Originally developed at Bell Labs by John Chambers and colleagues in the late 1970s, S is a commercial package widely used in the statistics community and in quantitative branches of the social and behavioral sciences. R is an open-source alternative to S that is largely though not entirely compatible with its commercial cousin. Among other things, R supports multidimensional array and list types, array slice operations, user-defined infix operators, call-by-need parameters, first-class functions, and unlimited extent.

14.2.4 “粘合”语言和通用脚本

14.2.4 “Glue” Languages and General-Purpose Scripting

脚本语言从文本处理祖先那里继承了一套丰富的模式匹配和字符串操作机制。从命令解释器 shell 那里,它们继承了各种附加功能,包括简单的语法;灵活的类型;轻松创建和管理子程序,具有 I/O 重定向和访问完成状态;文件查询;轻松的交互式和基于文件的 I/O;轻松访问命令行参数、环境字符串、进程标识符、时钟等;以及自动启动解释器(# !约定)。如第 14.1.1 节所述,许多脚本语言都有可以交互接受命令的解释器。

From their text-processing ancestors, scripting languages inherit a rich set of pattern matching and string manipulation mechanisms. From command interpreter shells they inherit a wide variety of additional features including simple syntax; flexible typing; easy creation and management of subprograms, with I/O redirection and access to completion status; file queries; easy interactive and file-based I/O; easy access to command-line arguments, environment strings, process identifiers, time-of-day clock, and so on; and automatic interpreter start-up (the #! convention). As noted in Section 14.1.1, many scripting languages have interpreters that will accept commands interactively.

例 14.24

Example 14.24

Perl 中的“强制退出”脚本

“Force quit” script in Perl

shell 和文本处理机制的结合允许脚本语言准备子进程的输入并解析子进程的输出。举一个简单的例子,考虑图 14.5所示的(特定于 Unix 的)“强制退出”Perl 脚本。使用正则表达式作为参数调用该脚本,该脚本会识别用户当前正在运行的所有进程,这些进程的名称、进程 ID 或命令行参数与该正则表达式匹配。它会打印每个进程的信息,并提示用户是否应终止该进程。

The combination of shell and text-processing mechanisms allows a scripting language to prepare input to, and parse output from, subsidiary processes. As a simple example, consider the (Unix-specific) “force quit” Perl script shown in Figure 14.5. Invoked with a regular expression as argument, the script identifies all of the user's currently running processes whose name, process id, or command-line arguments match that regular expression. It prints the information for each, and prompts the user for an indication of whether the process should be killed.

f14-05-9780124104099
图 14.5 Perl 中的脚本用于“强制退出”错误进程。Perl的文本处理功能允许我们解析ps的输出,而不是通过sedawk等外部工具对其进行过滤。

代码的第二行启动一个辅助进程来执行 Unix ps命令。命令行参数使ps打印当前用户拥有的所有进程的进程 ID 和名称,以及它们的完整命令行参数。命令末尾的管道符号 (|) 表示ps的输出将通过PS文件句柄提供给脚本。然后,主while循环遍历此输出的各行。在循环中,if条件将每一行与$ARGV[0](脚本命令行上提供的正则表达式)进行匹配。它还将行的第一个字(进程 ID)与$$(当前运行脚本的 Perl 解释器的 id)进行比较。

The second line of the code starts a subsidiary process to execute the Unix ps command. The command-line arguments cause ps to print the process id and name of all processes owned by the current user, together with their full command-line arguments. The pipe symbol (|) at the end of the command indicates that the output of ps is to be fed to the script through the PS file handle. The main while loop then iterates over the lines of this output. Within the loop, the if condition matches each line against $ARGV[0], the regular expression provided on the script's command line. It also compares the first word of the line (the process id) against $$, the id of the Perl interpreter currently running the script.

标量变量(在 Perl 中包括字符串)以美元符号($)开头。数组以 at 符号(@ )开头。在图 14.5中的while循环的第一行中,输入行($_,隐式)被拆分为以空格分隔的单词,然后这些单词被分配到数组@words 中。在下一行中,$words[0]指的是此数组的第一个元素,即标量。单个变量名在解释为标量、数组、哈希表、子例程或文件句柄时可能具有不同的值。解释的选择取决于前导标点符号和名称出现的上下文。我们将在第 14.4.3 节中详细介绍 Perl 中的上下文。■

Scalar variables (which in Perl include strings) begin with a dollar sign ($). Arrays begin with an at sign (@). In the first line of the while loop in Figure 14.5, the input line ($_, implicitly) is split into space-separated words, which are then assigned into the array @words. In the following line, $words[0] refers to the first element of this array, a scalar. A single variable name may have different values when interpreted as a scalar, an array, a hash table, a subroutine, or a file handle. The choice of interpretation depends on the leading punctuation mark and on the context in which the name appears. We shall have more to say about context in Perl in Section 14.4.3. ■

除了结合使用 shell 和文本处理机制之外,典型的胶水语言还提供了丰富的内置操作库,用于访问底层操作系统的功能,包括文件、目录和 I/O;进程和进程组;保护和授权;进程间通信和同步;定时和信号;以及套接字、名称服务和网络通信。正如文本处理机制最大限度地减少了使用sedawkgrep等外部工具的需要一样,操作系统内置函数最大限度地减少了对其他外部工具的需求。

Beyond the combination of shell and text-processing mechanisms, the typical glue language provides an extensive library of built-in operations to access features of the underlying operating system, including files, directories, and I/O; processes and process groups; protection and authorization; interprocess communication and synchronization; timing and signals; and sockets, name service, and network communication. Just as text-processing mechanisms minimize the need to employ external tools like sed, awk, and grep, operating system builtins minimize the need for other external tools.

与此同时,随着时间的推移,脚本语言已经开发出一套丰富的内部计算功能。大多数脚本语言对数学的支持都比 shell 中的数学支持好得多。包括 Scheme、Python 和 Ruby 在内的几种脚本语言都支持任意精度算术。大多数脚本语言都为高级类型提供广泛的支持,包括数组、字符串、元组、列表和哈希(关联数组)。一些脚本语言支持类和面向对象。一些脚本语言支持迭代器、延续、线程、反射以及一等函数和高阶函数。包括 Perl、Python 和 Ruby 在内的一些脚本语言支持模块和动态加载,用于“大规模编程”。这些功能可以最大限度地增加脚本语言本身可以编写的代码量,并最大限度地减少使用更传统的编译语言的需要。

At the same time, scripting languages have, over time, developed a rich set of features for internal computation. Most have significantly better support for mathematics than is typically found in a shell. Several, including Scheme, Python, and Ruby, support arbitrary precision arithmetic. Most provide extensive support for higher-level types, including arrays, strings, tuples, lists, and hashes (associative arrays). Several support classes and object orientation. Some support iterators, continuations, threads, reflection, and first-class and higher-order functions. Some, including Perl, Python, and Ruby, support modules and dynamic loading, for “programming in the large.” These features serve to maximize the amount of code that can be written in the scripting language itself, and to minimize the need to escape to a more traditional, compiled language.

总而言之,通用脚本的理念是尽可能简单地构建程序的整体框架,仅在执行特殊用途任务时才使用外部工具,仅在性​​能至关重要时才使用编译语言。

In summary, the philosophy of general-purpose scripting is to make it as easy as possible to construct the overall framework of a program, escaping to external tools only for special-purpose tasks, and to compiled languages only when performance is at a premium.

Python

Python

如第 14.1 节所述,Rexx 被普遍认为是第一种通用脚本语言,比 Perl 和 Tcl 早了近十年。Perl 和 Tcl 大致同时代:它们都是在 20 世纪 80 年代末开发的。Perl 最初用于粘合和文本处理应用程序。Tcl 最初是一种扩展语言,但很快也发展成为粘合应用程序。随着脚本在 20 世纪 90 年代的流行,用户开始开发其他语言,提供更多功能,满足特定应用领域的需求(后面的部分将对此进行详细介绍),或者支持更符合其设计者个人品味的编程风格。

As noted in Section 14.1, Rexx is generally considered the first of the general-purpose scripting languages, predating Perl and Tcl by almost a decade. Perl and Tcl are roughly contemporaneous: both were initially developed in the late 1980s. Perl was originally intended for glue and text-processing applications. Tcl was originally an extension language, but soon grew into glue applications as well. As the popularity of scripting grew in the 1990s, users were motivated to develop additional languages, to provide additional features, address the needs of specific application domains (more on this in subsequent sections), or support a style of programming more in keeping with the personal taste of their designers.

Python 最初由 Guido van Rossum 于 20 世纪 90 年代初在荷兰阿姆斯特丹的 CWI 开发。他于 1995 年开始在弗吉尼亚州雷斯顿的 CNRI 继续工作。经过一系列后续变动后,他于 2005 年加入 Google。该语言的最新版本归 Python 软件基金会所有。所有版本都是开源的。

Python was originally developed by Guido van Rossum at CWI in Amsterdam, the Netherlands, in the early 1990s. He continued his work at CNRI in Reston, Virginia, beginning in 1995. After a series of subsequent moves, he joined Google in 2005. Recent versions of the language are owned by the Python Software Foundation. All releases are open source.

例 14.25

Example 14.25

Python 中的“Forcequit”脚本

“Forcequit” script in Python

图 14.6展示了示例 14.24中的“强制退出”程序的 Python 版本。Python 从一开始就是一种面向对象的语言,这反映了编程语言设计的成熟。2包含一个与 Perl 一样丰富的标准库,但被划分为类似于 C++、Java 或 C# 的命名空间集合。脚本的第一行从 os 、re、subprocess、systime库模块导入符号。第五行将ps作为外部程序启动,并安排其输出通过 Unix 管道提供给脚本。我们通过调用输出管道的readline方法来丢弃来自子进程的初始(标题)输出行。然后,我们使用 Python for循环遍历剩余的行。

Figure 14.6 presents a Python version of the “force quit” program from Example 14.24. Reflecting the maturation of programming language design, Python was from the beginning an object-oriented language.2 It includes a standard library as rich as that of Perl, but partitioned into a collection of namespaces reminiscent of those of C++, Java, or C#. The first line of our script imports symbols from the os, re, subprocess, sys, and time library modules. The fifth line launches ps as an external program, and arranges for its output to be available to the script through a Unix pipe. We discard the initial (header) line of output from the subprocess by calling the output pipe's readline method. We then use a Python for loop to iterate over the remaining lines.

f14-06-9780124104099
图 14.6 使用 Python 3 编写脚本来“强制退出”错误进程。图 14.5进行比较。

Python 最显著的特征(尽管并非最重要的)可能是它依赖缩进进行语法分组。Python 不仅使用换行符来分隔命令;它还指定结构化语句的主体恰好由缩进一个制表位的后续语句组成。与 Perl 的“不止一种方法”哲学一样,Python 对缩进的使用往往会引起用户的强烈反应:一些人非常积极,一些人非常消极。

Perhaps the most distinctive feature of Python, though hardly the most important, is its reliance on indentation for syntactic grouping. Python not only uses line breaks to separate commands; it also specifies that the body of a structured statement consists of precisely those subsequent statements that are indented one more tab stop. Like the “more than one way to do it” philosophy of Perl, Python's use of indentation tends to arouse strong feelings among users: some strongly positive, some strongly negative.

正则表达式 ( re ) 库具有 Perl 中的所有功能,但使用了更为冗长的方法调用语法,而不是 Perl 的内置表示法。搜索例程返回一个“匹配对象”,该对象以惰性方式捕获字符串中出现模式的位置。如果未找到匹配项,则搜索将返回None 即空对象。在某种条件下,None被解释为假,而真正的匹配对象被解释为真。匹配对象又支持多种方法,包括group,它返回与第一个匹配相对应的子字符串。搜索的re.I标志表示不区分大小写。请注意,group返回一个字符串。与 Perl 和 Tcl 不同,Python 不会将其强制转换为整数——因此需要在for循环体的第二行进行显式类型转换。

The regular expression (re) library has all of the power available in Perl, but employs the somewhat more verbose syntax of method calls, rather than the built-in notation of Perl. The search routine returns a “match object” that captures, lazily, the places in the string at which the pattern appears. If no match is found, search returns None, the empty object, instead. In a condition, None is interpreted as false, while a true match object is interpreted as true. The match object in turn supports a variety of methods, including group, which returns the substring corresponding to the first match. The re.I flag to search indicates case insensitivity. Note that group returns a string. Unlike Perl and Tcl, Python will not coerce this to an integer—hence the need for the explicit type conversion on the second line of the body of the for loop.

与 Perl 一样,readline方法不会删除输入行末尾的换行符;我们使用rstrip方法来执行此操作。除非指定了不同的结束字符串,否则print函数会将换行符添加到其参数列表的末尾。除非明确指示flush,否则它还会缓冲其输出,等待调用操作系统,直到它拥有大量数据;对于交互式 IO,需要克服这一点。

As in Perl, the readline method does not remove the newline character at the end of an input line; we use the rstrip method to do this. The print function adds a newline to the end of its argument list unless given a different end string. Unless explicitly instructed to flush, it also tends to buffer its output, waiting to call the OS until it has a large amount of data; this needs to be defeated for interactive IO.

sleepkill例程在 Python 中作为内置库提供,就像在 Perl 中一样;无需启动单独的程序。当给定信号编号 0 时,kill会测试进程是否存在。但是,Python 的kill不会像在 Perl 中那样返回状态代码,而是在进程不存在时抛出异常。我们使用try块在预期情况下捕获此异常。■

The sleep and kill routines are available as library built-ins in Python, much as they are in Perl; there is no need to start a separate program. When given a signal number of 0, kill tests for process existence. Instead of returning a status code, however, as it does in Perl, Python's kill throws an exception if the process does not exist. We use a try block to catch this exception in the expected case. ■

虽然我们的“强制退出”程序至少可以部分传达出 Perl 和 Python 的“感觉”,但它无法捕捉到它们功能的广度。Python 包含前面章节中讨论的许多更有趣的功能,包括具有静态作用域的嵌套函数、lambda 表达式和高阶函数、真正的迭代器、列表推导、数组切片操作、反射、结构化异常处理、多重继承以及模块和动态加载。其中许多也出现在 Ruby 中。

While our “force quit” program may convey, at least in part, the “feel” of Perl and Python, it cannot capture the breadth of their capabilities. Python includes many of the more interesting features discussed in earlier chapters, including nested functions with static scoping, lambda expressions and higher-order functions, true iterators, list comprehensions, array slice operations, reflection, structured exception handling, multiple inheritance, and modules and dynamic loading. Many of these also appear in Ruby.

红宝石

Ruby

Ruby 是 20 世纪 90 年代初由 Yukihiro “Matz” Matsumoto 在日本开发的。Matz 写道,他“想要一种比 Perl 更强大、比 Python 更面向对象的语言”[ TFH13,前言]。第一个公开版本于 1995 年发布,并迅速在日本广受欢迎。随着 2001 年英文文档 [ TFH13,第 1 版] 的发布,Ruby 也在其他地方迅速传播。它的成功很大程度上要归功于 Ruby on Rails Web 开发框架。Rails 最初由 David Heinemeier Hansson 于 2004 年发布,随后被几家主要公司采用,尤其是 Apple(它将其包含在 Mac OS 10.5“Leopard”版本中)和 Twitter(它在其基础架构的早期版本中使用了它)。

Ruby was developed in Japan in the early 1990s by Yukihiro “Matz” Matsumoto. Matz writes that he “wanted a language more powerful than Perl, and more object-oriented than Python” [TFH13, Foreword]. The first public release was made available in 1995, and quickly gained widespread popularity in Japan. With the publication in 2001 of English-language documentation [TFH13, 1st ed.], Ruby spread rapidly elsewhere as well. Much of its success can be credited to the Ruby on Rails web-development framework. Originally released by David Heinemeier Hansson in 2004, Rails was subsequently adopted by several major players—notably Apple, which included it in the 10.5 “Leopard” release of the Mac OS, and Twitter, which used it for early versions of their infrastructure.

例 14.26

Example 14.26

Ruby 中的方法调用语法

Method call syntax in Ruby

为了与 Matz 最初的动机保持一致,Ruby 是一种纯粹的面向对象语言,就像 Smalltalk 一样:一切事物(甚至是内置类型的实例)都是对象。整数有超过 25 种内置方法。字符串有超过 75 种。它甚至支持 Smalltalk 式的语法:2*4 + 5是(2.*(4)).+(5) 的语法糖,而后者又等同于(2.send('*', 4 )).send(' + ', 5)。3 ■

In keeping with Matz's original motivation, Ruby is a pure object-oriented language, in the sense of Smalltalk: everything—even instances of built-in types—is an object. Integers have more than 25 built-in methods. Strings have more than 75. Smalltalk-like syntax is even supported: 2*4 + 5 is syntactic sugar for (2.*(4)).+(5), which is in turn equivalent to (2.send('*', 4)).send(' + ', 5).3

例 14.27

Example 14.27

Ruby 中的“强制退出”脚本

“Force quit” script in Ruby

图 14.7展示了我们的“强制退出”程序的 Ruby 版本。换行符用于结束当前语句,但缩进并不重要。标识符开头的美元符号 ( $ ) 表示全局名称。虽然此示例中未出现 at 符号 ( @ ),但它表示当前对象的实例变量。双 at 符号 ( @@ ) 表示当前类的实例变量

Figure 14.7 presents a Ruby version of our “force quit” program. Newline  characters serve to end the current statement, but indentation is not significant. A dollar sign ($) at the beginning of an identifier indicates a global name. Though it doesn't appear in this example, an at sign (@) indicates an instance variable of the current object. Double at signs (@@) indicate an instance variable of the current class.

f14-07-9780124104099
图 14.7 Ruby 脚本用于“强制退出”错误进程。与图 14.514.6比较。

图 14.7最显著的特征可能是它使用了和迭代器。IO.popen类方法以字符串作为参数,该字符串指定外部程序的名称和参数。该方法还以类似 Smalltalk 的方式接受关联块,该指定为用花括号分隔的多行 Ruby 代码片段。关联块本质上是popen 的一个额外参数,作为闭包传递。闭包由popen调用,将表示外部命令输出的文件句柄( IO类的对象)作为参数传递。块开头的| ps | 指定此句柄在块中的名称。类似地,对象ps的each方法是一个迭代器,它对每一行数据调用一次关联块(以|line|开头的大括号中的代码)。对于那些更熟悉传统for循环语法的人来说,迭代器也可以写成

Probably the most distinctive feature of Figure 14.7 is its use of blocks and iterators. The IO.popen class method takes as argument a string that specifies the name and arguments of an external program. The method also accepts, in a manner reminiscent of Smalltalk, an associated block, specified as a multiline fragment of Ruby code delimited with curly braces. The associated block is essentially an extra parameter to popen, passed as a closure. The closure is invoked by popen, passing as parameter a file handle (an object of class IO) that represents the output of the external command. The |ps| at the beginning of the block specifies the name by which this handle is known within the block. In a similar vein, the each method of object ps is an iterator that invokes the associated block (the code in braces beginning with |line|) once for every line of data. For those more comfortable with traditional for loop syntax, the iterator can also be written

用于 PS 中的线条

for line in PS

 

 

结尾

end

除了(真正的)迭代器之外,Ruby 还提供了延续、一等函数和高阶函数以及具有无限范围的闭包。它的模块机制支持混合继承的扩展形式。虽然类不能从模块继承数据成员,但它可以继承代码。运行时类型检查使这种继承或多或少变得简单明了。未明确包含在当前类中的模块的方法可以作为限定名访问;图 14.7中的Process.kill就是一个例子。方法sleepexit属于模块Kernel,它包含在类Object中,因此可以在任何地方使用而无需限定。与popen一样,它们是类方法,而不是实例方法;它们没有“当前对象”的概念。变量stdinstderr引用类IO的全局对象。

In addition to (true) iterators, Ruby provides continuations, first-class and higher-order functions, and closures with unlimited extent. Its module mechanism supports an extended form of mix-in inheritance. Though a class cannot inherit data members from a module, it can inherit code. Run-time type checking makes such inheritance more or less straightforward. Methods of modules that have not been explicitly included into the current class can be accessed as qualified names; Process.kill is an example in Figure 14.7. Methods sleep and exit belong to module Kernel, which is included by class Object, and is thus available everywhere without qualification. Like popen, they are class methods, rather than instance methods; they have no notion of “current object.” Variables stdin and stderr refer to global objects of class IO.

Ruby 中的正则表达式操作是Regexp类的方法,可以使用标准的面向对象语法调用。为方便起见,还支持类似 Perl 的表示法作为语法糖;我们在图 14.7中使用了这种表示法。

Regular expression operations in Ruby are methods of class Regexp, and can be invoked with standard object-oriented syntax. For convenience, Perl-like notation is also supported as syntactic sugar; we have used this notation in Figure 14.7.

最内层的beginend块的rescue子句是一个异常处理程序。如图14.6中的 Python 代码所示,它允许我们通过捕获在进程终止后尝试引用该进程时出现的(预期)异常来确定kill操作是否成功。■

The rescue clause of the innermost beginend block is an exception handler. As in the Python code of Figure 14.6, it allows us to determine whether the kill operation has succeeded by catching the (expected) exception that arises when we attempt to refer to a process after it has died. ■

14.2.5 扩展语言

14.2.5 Extension Languages

大多数应用程序都会接受某种命令,命令告诉它们要做什么。有时这些命令以文本形式输入;更常见的是它们由用户界面事件触发,例如鼠标单击、菜单选择和按键。图形绘制程序中的命令可能会保存或加载图形;选择、插入、删除或修改其部分;选择线条样式、粗细或颜色;缩放或旋转显示;或修改用户首选项。

Most applications accept some sort of commands, which tell them what to do. Sometimes these commands are entered textually; more often they are triggered by user interface events such as mouse clicks, menu selections, and keystrokes. Commands in a graphical drawing program might save or load a drawing; select, insert, delete, or modify its parts; choose a line style, weight, or color; zoom or rotate the display; or modify user preferences.

扩展语言允许用户创建新命令,通常使用现有命令作为构建块,从而提高应用程序的实用性。扩展语言被广泛认为是复杂工具的基本功能。Adobe 的图形套件(Illustrator、Photoshop、InDesign 等)可以使用 JavaScript、Visual Basic(在 Windows 上)或 AppleScript(在 Mac 上)进行扩展(编写脚本)。迪士尼和工业光魔使用 Python 来扩展其内部(专有)工具。计算机游戏行业大量使用 Lua 来编写商业和开源游戏引擎的脚本。许多商用工具,包括 AutoCAD、Maya、Director 和 Flash,都有自己独特的脚本语言。这个列表只是冰山一角。

An extension language serves to increase the usefulness of an application by allowing the user to create new commands, generally using the existing commands as building blocks. Extension languages are widely regarded as an essential feature of sophisticated tools. Adobe's graphics suite (Illustrator, Photoshop, InDesign, etc.) can be extended (scripted) using JavaScript, Visual Basic (on Windows), or AppleScript (on the Mac). Disney and Industrial Light & Magic use Python to extend their internal (proprietary) tools. The computer gaming industry makes heavy use of Lua for scripting of both commercial and open-source game engines. Many commercially available tools, including AutoCAD, Maya, Director, and Flash, have their own unique scripting languages. This list barely scratches the surface.

为了实现扩展,工具必须

To admit extension, a tool must

 结合脚本语言解释器,或与脚本语言解释器进行通信。

 incorporate, or communicate with, an interpreter for a scripting language.

 提供允许脚本调用该工具现有命令的挂钩。

 provide hooks that allow scripts to call the tool's existing commands.

 允许用户将新定义的命令与用户界面事件联系起来。

 allow the user to tie newly defined commands to user interface events.

只要小心谨慎,这些机制就可以独立于任何特定的脚本语言。微软的 Windows 脚本接口允许使用几乎任何语言来编写操作系统、Web 服务器和浏览器的脚本。GIMP 是广泛使用的 GNU 图像处理程序,具有类似的通用接口:它带有一个 Scheme 方言的内置解释器,并支持 Perl 和 Tcl 等插件(外部提供的解释器模块)。当然,用户社区倾向于使用一种最喜欢的语言,以促进代码共享。Microsoft 工具通常使用 PowerShell 编写脚本;GIMP 使用 Scheme;Adobe 工具在 PC 上用 Visual Basic 编写,在 Mac 上用 AppleScript 编写。

With care, these mechanisms can be made independent of any particular scripting language. Microsoft's Windows Script interface allows almost any language to be used to script the operating system, web server, and browser. GIMP, the widely used GNU Image Manipulation Program, has a comparably general interface: it comes with a built-in interpreter for a dialect of Scheme, and supports plug-ins (externally provided interpreter modules) for Perl and Tcl, among others. There is a tendency, of course, for user communities to converge on a favorite language, to facilitate sharing of code. Microsoft tools are usually scripted with PowerShell; GIMP with Scheme; Adobe tools with Visual Basic on the PC, or AppleScript on the Mac.

现存最古老的扩展机制之一是用于编写本书的emacs文本编辑器。为emacs创建了大量扩展包;其中许多都默认安装在标准发行版中。事实上,用户认为的编辑器核心功能中的大部分实际上是由扩展提供的;真正内置的部分相对较小。

One of the oldest existing extension mechanisms is that of the emacs text editor, used to write this book. An enormous number of extension packages have been created for emacs; many of them are installed by default in the standard distribution. In fact much of what users consider the editor's core functionality is actually provided by extensions; the truly built-in parts are comparatively small.

例 14.28

Example 14.28

使用 Emacs Lisp 对行进行编号

Numbering lines with Emacs Lisp

emacs的扩展语言是 Lisp 的一种方言,称为 Emacs Lisp,或简称 elisp。图 14.8中显示了一个示例脚本。它假设用户使用标准标记机制选择了一段文本。然后,它会在该区域的每一行开头插入行号。第一行默认编号为 1,但可以使用可选参数指定其他起始编号。行号用前缀和后缀括起来,默认情况下为“ “(空)和“ ) “,但用户可以根据需要更改。为了保持现有的对齐方式,小数字会在左侧填充足够的空格以匹配最后一行数字的宽度。

The extension language for emacs is a dialect of Lisp called Emacs Lisp, or elisp, for short. An example script appears in Figure 14.8. It assumes that the user has used the standard marking mechanism to select a region of text. It then inserts a line number at the beginning of every line in the region. The first line is numbered 1 by default, but an alternative starting number can be specified with an optional parameter. Line numbers are bracketed with a prefix and suffix that are “ “ (empty) and “)“ by default, but can be changed by the user if desired. To maintain existing alignment, small numbers are padded on the left with enough spaces to match the width of the number on the final line.

f14-08-9780124104099
图 14.8 Emacs Lisp 函数用于对选定文本区域中的行进行编号。

在这个例子中,可以看到 Emacs Lisp 的许多特性。setq -default命令是一个赋值,它在当前缓冲区(编辑会话)和任何未明确覆盖先前值的并发缓冲区中都可见。defun命令定义了一个新命令。它的参数依次为命令名称、形式参数列表、文档字符串、交互式规范和主体。number -region的参数列表包括当前标记区域的起始和终止位置,以及可选的初始行号。文档字符串会自动合并到在线帮助系统。交互式规范控制通过用户界面调用命令时如何传递参数。(也可以从其他脚本调用该命令,在这种情况下,参数以常规方式传递。)如果缓冲区是只读的, “ * ”会引发异常。“ r ”表示当前标记区域的开始和结束。“ \n ”将“ r ”与后面的“ p ”分开,后者表示可选的数字前缀参数。当命令绑定到按键时,可以通过在按键前加上“ Cu 10 ”(control-U 10)来指定前缀参数,例如 10 。

Many features of Emacs Lisp can be seen in this example. The setq-default command is an assignment that is visible in the current buffer (editing session) and in any concurrent buffers that haven't explicitly overridden the previous value. The defun command defines a new command. Its arguments are, in order, the command name, formal parameter list, documentation string, interactive specification, and body. The argument list for number-region includes the start and end locations of the currently marked region, and the optional initial line number. The documentation string is automatically incorporated into the online help system. The interactive specification controls how arguments are passed when the command is invoked through the user interface. (The command can also be called from other scripts, in which case arguments are passed in the conventional way.) The “*“ raises an exception if the buffer is read-only. The “r“ represents the beginning and end of the currently marked region. The “\n“ separates the “r“ from the following “p,” which indicates an optional numeric prefix argument. When the command is bound to a keystroke, a prefix argument of, say, 10 can be specified by preceding the keystroke with “C-u 10“ (control-U 10).

与 Lisp 中的惯例一样,let*命令引入了一组局部变量,其中列表 ( fmt ) 中的后续条目可以引用先前的条目 ( num-lines )。标记是缓冲区中的索引,当文本插入到其前面时,它会自动更新以保持其位置。我们创建了结束标记,以便新插入的行号不会改变我们对待编号区域结束位置的概念。我们在脚本末尾将finish设置为nil,以使emacs无需从现在开始到垃圾收集器开始回收它之间不断更新标记。

As usual in Lisp, the let* command introduces a set of local variables in which later entries in the list (fmt) can refer to earlier entries (num-lines). A marker is an index into the buffer that is automatically updated to maintain its position when text is inserted in front of it. We create the finish marker so that newly inserted line numbers do not alter our notion of where the to-be-numbered region ends. We set finish to nil at the end of the script to relieve emacs of the need to keep updating the marker between now and whenever the garbage collector gets around to reclaiming it.

format命令类似于C 中的sprintf 。我们在fmt声明中使用过一次,insert调用中也使用过一次,将所有行号填充到适当的长度。save -excursion命令大致相当于一个异常处理程序(例如 Java try块),带有一个finally子句,用于恢复当前关注焦点((point))和标记区域的边界。

The format command is similar to sprintf in C. We have used it, once in the declaration of fmt and again in the call to insert, to pad all line numbers out to an appropriate length. The save-excursion command is roughly equivalent to an exception handler (e.g., a Java try block) with a finally clause that restores the current focus of attention ((point)) and the borders of the marked region.

我们的脚本可以通过以下方式提供给emacs:将其包含在个人启动文件(通常是~/.emacs)、使用交互式load-file命令读取它所在的其他文件,或者将其加载到缓冲区、将注意力焦点放在它后面,然后执行交互式eval-last-sexp命令。完成上述任何一项后,我们可以通过键入Mx number-region <RET>(meta-X,后跟命令名称和回车键)以交互方式调用我们的命令。或者,我们可以将命令绑定到键盘快捷键:

Our script can be supplied to emacs by including it in a personal start-up file (usually ~/.emacs), by using the interactive load-file command to read some other file in which it resides, or by loading it into a buffer, placing the focus of attention immediately after it, and executing the interactive eval-last-sexp command. Once any of these has been done, we can invoke our command interactively by typing M-x number-region <RET> (meta-X, followed by the command name and the return key). Alternatively, we can bind our command to a keyboard shortcut:

(定义键全局映射 [?\C-#]'数字区域)

(define-key global-map [?\C-#] 'number-region)

此单行脚本以上述任何一种方式执行,将我们的number-region命令绑定到组合键“control-number-sign”。■

This one-line script, executed in any of the ways described above, binds our number-region command to key combination “control-number-sign”. ■

14-01-9780124104099检查你的理解

Check Your Understanding

11.  sed中的模式空间是什么意思?

11. What is meant by the pattern space in sed?

12. 简要描述awk字段关联数组

12. Briefly describe the fields and associative arrays of awk.

13. 早期版本的 Perl 在哪些方面对sedawk进行了改进?

13. In what ways did even early versions of Perl improve on sed and awk?

14.解释一下 Perl 中 while循环和文件句柄之间的特殊关系。空文件句柄 <> 是什么意思?

14. Explain the special relationship between while loops and file handles in Perl. What is the meaning of the empty file handle, <>?

15. 说出三种广泛使用的数学计算商业软件包。

15. Name three widely used commercial packages for mathematical computing.

16. 列出 R 统计脚本语言的几个显著特点。

16. List several distinctive features of the R statistical scripting language.

17.解释 Perl 中变量名开头的$@字符的含义。解释Ruby 中$@@@的不同含义。

17. Explain the meaning of the $ and @ characters at the beginning of variable names in Perl. Explain the different meanings of $, @, and @@ in Ruby.

18.  14.2.4 节中描述的哪种语言使用缩进来控制语法分组?

18. Which of the languages described in Section 14.2.4 uses indentation to control syntactic grouping?

19. 列出 Python 的几个显著特点。

19. List several distinctive features of Python.

20. 简要描述 Ruby 如何使用迭代器

20. Describe, briefly, how Ruby uses blocks and iterators.

21. 脚本语言必须提供哪些功能才能用于扩展?

21. What capabilities must a scripting language provide in order to be used for extension?

22. 说出几种使用扩展语言的商业工具。

22. Name several commercial tools that use extension languages.

14.3 编写万维网脚本

14.3 Scripting the World Wide Web

万维网上的大部分内容(尤其是搜索引擎可见的内容)都是静态的:页面很少甚至根本不会发生变化。但超文本(Web 所基于的抽象概念)始终被认为是一种表示“复杂、变化和不确定”的方式 [ Nel65 ]。当今 Web 的大部分功能在于它能够提供移动的页面、播放声音的页面、响应用户操作的页面,或者(也许最重要的是)响应页面获取请求,提供按需创建或格式化的信息。

Much of the content of the World Wide Web—particularly the content that is visible to search engines—is static: pages that seldom, if ever, change. But hypertext, the abstract notion on which the Web is based, was always conceived as a way to represent “the complex, the changing, and the indeterminate” [Nel65]. Much of the power of the Web today lies in its ability to deliver pages that move, play sounds, respond to user actions, or—perhaps most important—contain information created or formatted on demand, in response to the page-fetch request.

从编程语言的角度来看,简单地播放录制的音频或视频并不是特别有趣。因此,我们在这里将注意力集中在与 Internet URI(统一资源标识符)相关联的程序(脚本)动态生成的内容上。4假设我们在客户端计算机上的浏览器中输入 URI,然后浏览器向相应的 Web 服务器发送请求。如果内容是动态创建的,那么显然第一个问题是:创建它的脚本是在服务器上运行还是在客户端计算机上运行?这些选项分别称为服务器端客户端Web 脚本。

From a programming languages point of view, simple playback of recorded audio or video is not particularly interesting. We therefore focus our attention here on content that is generated on the fly by a program—a script—associated with an Internet URI (uniform resource identifier).4 Suppose we type a URI into a browser on a client machine, and the browser sends a request to the appropriate web server. If the content is dynamically created, an obvious first question is: does the script that creates it run on the server or the client machine? These options are known as server-side and client-side web scripting, respectively.

服务器端脚本通常用于服务提供商希望完全控制页面内容,但无法(或不想)提前创建内容的情况。示例包括搜索引擎、互联网零售商、拍卖网站以及任何为客户提供个人帐户在线访问权限的组织返回的页面。客户端脚本通常用于不需要访问专有信息的任务,如果在客户端的机器上执行,效率会更高。示例包括交互式动画、填写表格的错误检查以及各种其他独立计算。

Server-side scripts are typically used when the service provider wants to retain complete control over the content of the page, but can't (or doesn't want to) create the content in advance. Examples include the pages returned by search engines, Internet retailers, auction sites, and any organization that provides its clients with on-line access to personal accounts. Client-side scripts are typically used for tasks that don't need access to proprietary information, and are more efficient if executed on the client's machine. Examples include interactive animation, error-checking of fill-in forms, and a wide variety of other self-contained calculations.

14.3.1 CGI 脚本

14.3.1 CGI Scripts

服务器端 Web 脚本的原始机制是通用网关接口 (CGI)。CGI 脚本是驻留在 Web 服务器程序已知的特殊目录中的可执行程序。当客户端请求与此类程序相对应的 URI 时,服务器将执行该程序并将其输出发送回客户端。当然,此输出必须是浏览器可以理解的内容 — 通常是 HTML。

The original mechanism for server-side web scripting was the Common Gateway Interface (CGI). A CGI script is an executable program residing in a special directory known to the web server program. When a client requests the URI corresponding to such a program, the server executes the program and sends its output back to the client. Naturally, this output needs to be something that the browser will understand—typically HTML.

例 14.29

Example 14.29

使用 CGI 脚本进行远程监控

Remote monitoring with a CGI script

CGI 脚本可以用服务器机器上可用的任何语言编写,但 Perl 特别流行:它的字符串处理和“粘合”机制非常适合生成 HTML,并且在 Web 早期就已经广泛使用。作为一个简单但有些人为的例子,假设我们希望能够监视某个用户社区共享的服务器机器的状态。图 14.9中的 Perl 脚本创建了一个网页,该网页以服务器机器的名称为标题,包含uptimewho命令(两个简单的状态信息源)的输出。脚本的初始print命令生成一个 HTTP 消息头,指示接下来的内容是 HTML。执行脚本的示例输出如图14.10所示。■

CGI scripts may be written in any language available on the server's machine, though Perl is particularly popular: its string-handling and “glue” mechanisms are ideally suited to generating HTML, and it was already widely available during the early years of the Web. As a simple if somewhat artificial example, suppose we would like to be able to monitor the status of a server machine shared by some community of users. The Perl script in Figure 14.9 creates a web page titled by the name of the server machine, and containing the output of the uptime and who commands (two simple sources of status information). The script's initial print command produces an HTTP message header, indicating that what follows is HTML. Sample output from executing the script appears in Figure 14.10. ■

f14-09-9780124104099
图 14.9 用 Perl 编写的简单 CGI 脚本。如果此脚本名为status.perl,并安装在服务器的cgi-bin目录中,则 Internet 上任何位置的用户都可以通过在浏览器窗口中键入hostname/cgi-bin/status.perl 来获取摘要统计信息和当前登录到服务器的用户列表。
f14-10-9780124104099
图 14.10 图 14.9 脚本的示例输出。HTML 源代码显示在顶部;呈现的页面如下。

例 14.30

Example 14.30

带有 CGI 脚本的Adder Web 表单

Adder web form with a CGI script

CGI 脚本通常用于处理在线表单。图 14.11中显示了一个简单的示例。HTML文件中的form元素指定 CGI 脚本的 URI,当用户单击“提交”按钮时将调用该脚本。先前输入到输入字段中的值将作为 URI 的尾部(对于get类型表单)或标准输入流(对于post类型表单,如下所示)传递给脚本。5无论使用哪种方法,我们都可以使用在脚本开头加载的标准CGI Perl 库的param例程访问这些值。■

CGI scripts are commonly used to process on-line forms. A simple example appears in Figure 14.11. The form element in the HTML file specifies the URI of the CGI script, which is invoked when the user hits the Submit button. Values previously entered into the input fields are passed to the script either as a trailing part of the URI (for a get-type form) or on the standard input stream (for a post-type form, shown here).5 With either method, we can access the values using the param routine of the standard CGI Perl library, loaded at the beginning of our script. ■

f14-11-9780124104099
图 14.11 交互式 CGI 表单。原始网页的源代码显示在左上角,渲染后的页面显示在右侧。用户在文本字段中输入了 12 和 34。按下“提交”按钮后,客户端浏览器会向服务器发送请求,请求 URI /cgi-bin/add.perl。请求中包含值 12 和 1 3。中间显示的 Perl 脚本使用这些值生成新的网页,以 HTML 格式显示在左下角,渲染后的页面显示在右侧。

14.3.2 嵌入式服务器端脚本

14.3.2 Embedded Server-Side Scripts

尽管 CGI 脚本被广泛使用,但它有几个缺点:

Though widely used, CGI scripts have several disadvantages:

 Web 服务器必须将每个脚本作为单独的程序启动,这可能会带来很大的开销(尽管编译为本机代码的 CGI 脚本一旦运行就会非常快)。

 The web server must launch each script as a separate program, with potentially significant overhead (though a CGI script compiled to native code can be very fast once running).

 因为服务器几乎无法控制脚本的行为,所以脚本通常必须由受信任的系统管理员安装在受信任的目录中;它们不能像普通页面一样驻留在任意位置。

 Because the server has little control over the behavior of a script, scripts must generally be installed in a trusted directory by trusted system administrators; they cannot reside in arbitrary locations as ordinary pages do.

 脚本的名称出现在URI中,通常以受信任目录的名称作为前缀,因此静态和动态页面对于最终用户来说看起来是不同的。

 The name of the script appears in the URI, typically prefixed with the name of the trusted directory, so static and dynamic pages look different to end users.

 每个脚本不仅必须生成动态内容,还必须生成格式化和显示动态内容所需的 HTML 标记。这些额外的“样板”使得脚本更难编写。

 Each script must generate not only dynamic content but also the HTML tags that are needed to format and display it. This extra “boilerplate” makes scripts more difficult to write.

为了解决这些缺点,大多数 Web 服务器都提供了“模块加载”机制,允许将一种或多种脚本语言的解释器合并到服务器本身中。然后可以将受支持语言的脚本嵌入到“普通”网页中。Web 服务器直接解释此类脚本,而无需启动外部程序。然后,它会用脚本生成的输出替换它们,然后再将页面发送到客户端。客户端甚至无法知道脚本的存在。

To address these disadvantages, most web servers provide a “module-loading” mechanism that allows interpreters for one or more scripting languages to be incorporated into the server itself. Scripts in the supported language(s) can then be embedded in “ordinary” web pages. The web server interprets such scripts directly, without launching an external program. It then replaces the scripts with the output they produce, before sending the page to the client. Clients have no way to even know that the scripts exist.

可嵌入的服务器端脚本语言包括 PHP、PowerShell(在 Microsoft Active Server Pages 中)、Ruby、Cold Fusion(来自 Macromedia Corp.)和 Java(通过 Java Server Pages 中的“Servlet”)。其中最常见的是 PHP。尽管 PHP 源自 Perl,但它已针对其目标域进行了广泛的定制,内置了对电子邮件和 MIME 编码、所有标准 Internet 通信协议、身份验证和安全、HTML 和 URI 操作以及与数十个数据库系统的交互等的支持。

Embeddable server-side scripting languages include PHP, PowerShell (in Microsoft Active Server Pages), Ruby, Cold Fusion (from Macromedia Corp.), and Java (via “Servlets” in Java Server Pages). The most common of these is PHP. Though descended from Perl, PHP has been extensively customized for its target domain, with built-in support for (among other things) e-mail and MIME encoding, all the standard Internet communication protocols, authentication and security, HTML and URI manipulation, and interaction with dozens of database systems.

例 14.31

Example 14.31

使用 PHP 脚本进行远程监控

Remote monitoring with a PHP script

图 14.9的 PHP 等效代码如图14.12所示。图中的大部分文本都是标准 HTML。PHP 代码嵌入在<?php?>分隔符之间。这些分隔符本身不是 HTML;而是指示需要由 PHP 解释器执行以生成替换文本的处理指令。因此,页面的“样板”部分可以逐字显示;它们不需要由print(Perl)或echo(PHP)命令生成。请注意,单独的脚本片段是单个程序的一部分。例如,$host变量在第一个片段中设置,并在第二个片段中再次使用。■

The PHP equivalent of Figure 14.9 appears in Figure 14.12. Most of the text in this figure is standard HTML. PHP code is embedded between <?php and ?> delimiters. These delimiters are not themselves HTML; rather, they indicate a processing instruction that needs to be executed by the PHP interpreter to generate replacement text. The “boilerplate” parts of the page can thus appear verbatim; they need not be generated by print (Perl) or echo (PHP) commands. Note that the separate script fragments are part of a single program. The $host variable, for example, is set in the first fragment and used again in the second. ■

f14-12-9780124104099
图 14.12 网页中嵌入的简单 PHP 脚本。当由支持 PHP 的主机提供服务时,该页面执行的操作与图 14.9中的 CGI 脚本相同。

例 14.32

Example 14.32

碎片化的 PHP 脚本

A fragmented PHP script

PHP 脚本甚至可以在结构化语句中间被拆分成片段。图 14.13包含一个脚本,其中iffor语句跨越多个片段。实际上,一个脚本片段的结尾和下一个脚本片段的开头之间的 HTML 文本的行为就像是由echo命令输出的一样。Web 设计人员可以自由使用任何方法(echo或转义为原始 HTML),这些方法对于手头的任务来说似乎最方便。■

PHP scripts can even be broken into fragments in the middle of structured statements. Figure 14.13 contains a script in which if and for statements span fragments. In effect, the HTML text between the end of one script fragment and the beginning of the next behaves as if it had been output by an echo command. Web designers are free to use whichever approach (echo or escape to raw HTML) seems most convenient for the task at hand. ■

f14-13-9780124104099
图 14.13 一个片段化的 PHP 脚本。尽管中间有原始 HTML,但 if 和 for 语句仍按预期工作。当浏览器请求时,此页面会显示从 0 到 19 的数字,奇数以粗体显示。

自行发布表格

Self-Posting Forms

例 14.33

Example 14.33

带有 PHP 脚本的Adder Web 表单

Adder web form with a PHP script

通过改变FORM元素的action属性,我们可以让图14.11中的Adder页面调用PHP脚本而不是CGI脚本:

By changing the action attribute of the FORM element, we can arrange for the Adder page of Figure 14.11 to invoke a PHP script instead of a CGI script:

<form action=“add.php” method=“post”>

<form action=“add.php” method=“post”>

PHP 脚本本身如图 14.14的上半部分所示。表单值在名为_REQUEST的关联数组(哈希表)中提供给脚本。不需要任何特殊库。■

The PHP script itself is shown in the top half of Figure 14.14. Form values are made available to the script in an associative array (hash table) named _REQUEST. No special library is required. ■

f14-14-9780124104099
图 14.14 交互式 PHP 网页。顶部的脚本可以替代图 14.11中间的脚本。当前图中的下方脚本替代了顶部的网页和图 14.11中间的脚本。它会检查是否已收到完整的参数集。如果没有,它会显示填写表格;如果已收到,它会显示结果。

例 14.34

Example 14.34

自发布加法器网络表单

Self-posting Adder web form

由于我们的 PHP 脚本由 Web 服务器直接执行,因此它可以安全地驻留在任意 Web 目录中,包括Adder页面所在的目录。事实上,通过检查页面的请求方式,我们可以将表单和脚本合并为一个页面,并让它处理自己的请求!我们在图 14.14的下半部分说明了此选项。■

Because our PHP script is executed directly by the web server, it can safely reside in an arbitrary web directory, including the one in which the Adder page resides. In fact, by checking to see how a page was requested, we can merge the form and the script into a single page, and let it service its own requests! We illustrate this option in the bottom half of Figure 14.14. ■

14.3.3 客户端脚本

14.3.3 Client-Side Scripts

虽然嵌入式服务器端脚本通常比 CGI 脚本更快,至少在启动成本占主导地位时,但 Internet 上的通信对于真正交互式的页面来说仍然太慢。如果我们想让页面的行为或外观随着用户移动鼠标、点击、键入或隐藏或显示窗口而改变,我们确实需要在客户端计算机上执行某种脚本。

While embedded server-side scripts are generally faster than CGI scripts, at least when start-up cost predominates, communication across the Internet is still too slow for truly interactive pages. If we want the behavior or appearance of the page to change as the user moves the mouse, clicks, types, or hides or exposes windows, we really need to execute some sort of script on the client's machine.

由于 CGI 脚本和可嵌入的服务器端脚本(在较小程度上)在 Web 设计人员的站点上运行,因此它们可以用多种不同的语言编写。客户端看到的只是标准 HTML。相比之下,客户端脚本需要客户端机器上的解释器。由于 JavaScript 历来“在正确的时间出现在正确的地点”,因此几乎世界上所有的 Web 浏览器都至少在一定程度上一致地支持 JavaScript。鉴于仍在运行的旧版浏览器数量,以及说服用户升级或安装新插件的难度,旨在供有限域(例如,单个公司的桌面)之外使用的页面几乎总是使用 JavaScript 来实现交互功能。

Because they run on the web designer's site, CGI scripts and, to a lesser extent, embeddable server-side scripts can be written in many different languages. All the client ever sees is standard HTML. Client-side scripts, by contrast, require an interpreter on the client's machine. By virtue of having been “in the right place at the right time” historically, JavaScript is supported with at least some degree of consistency by almost all of the world's web browsers. Given the number of legacy browsers still running, and the difficulty of convincing users to upgrade or to install new plug-ins, pages intended for use outside a limited domain (e.g., the desktops of a single company) almost always use JavaScript for interactive features.

例 14.35

Example 14.35

使用 JavaScript 编写的Adder Web 表单

Adder web form in JavaScript

图 14.15显示了嵌入了 JavaScript 的页面,该页面在客户端模仿了图 14.1114.14中 Adder 脚本的行为。函数doAdd定义在页面的标题中,因此在整个过程中都可用。特别是,当用户单击“计算”按钮时,将调用该函数。默认情况下,输入值是字符串;我们使用parseInt函数将它们转换为整数。最后的赋值语句中(argA + argB)周围的括号强制使用整数加法。其他出现的+是字符串连接。为了禁用当用户按下 Enter 或 Return 键时将输入数据提交到服务器的通常机制,我们为表单的onsubmit属性指定了一种虚拟行为。

Figure 14.15 shows a page with embedded JavaScript that imitates (on the client) the behavior of the Adder scripts of Figures 14.11 and 14.14. Function doAdd is defined in the header of the page so it is available throughout. In particular, it will be invoked when the user clicks on the Calculate button. By default, the input values are character strings; we use the parseInt function to convert them to integers. The parentheses around (argA + argB) in the final assignment statement then force the use of integer addition. The other occurrences of + are string concatenation. To disable the usual mechanism whereby input data are submitted to the server when the user hits the enter or return key, we have specified a dummy behavior for the onsubmit attribute of the form.

f14-15-9780124104099
图 14.15 交互式 JavaScript 网页。左侧显示源代码。右侧的渲染版本显示了用户输入两个值并点击“计算”按钮后页面的外观,从而显示输出消息。通过输入新值并再次单击,用户可以计算任意数量的总和。每个新计算都将替换输出消息。

在我们的 JavaScript 版本中,我们没有像 CGI 和 PHP 脚本那样用输出文本替换页面,而是选择将输出附加到页面底部。HTML SPAN元素在文档中提供了一个可插入此输出的命名位置,而getElementByld JavaScript 方法为我们提供了对此元素的引用。由万维网联盟标准化的 HTML文档对象模型 (DOM)指定了大量其他元素、属性和用户操作,所有这些都可以在 JavaScript 中访问。通过它们,脚本可以在适当的时候检查或更改页面内容、结构或样式的几乎任何方面。■

Rather than replace the page with output text, as our CGI and PHP scripts did, we have chosen in our JavaScript version to append the output at the bottom. The HTML SPAN element provides a named place in the document where this output can be inserted, and the getElementByld JavaScript method provides us with a reference to this element. The HTML Document Object Model (DOM), standardized by the World Wide Web Consortium, specifies a very large number of other elements, attributes, and user actions, all of which are accessible in JavaScript. Through them, scripts can, at appropriate times, inspect or alter almost any aspect of the content, structure, or style of a page. ■

14.3.4 Java 小程序和其他嵌入式元素

14.3.4 Java Applets and Other Embedded Elements

作为要求客户端脚本与网页的 DOM 交互的替代方案,许多浏览器都支持嵌入机制,该机制允许浏览器插件负责页面的某些矩形区域,然后它可以在该区域显示所需的任何内容。换句话说,插件与其说是编写浏览器脚本,不如说是完全绕过浏览器。从历史上看,插件广泛用于 HTML 不太支持的内容(尤其是动画和视频)。

As an alternative to requiring client-side scripts to interact with the DOM of a web page, many browsers support an embedding mechanism that allows a browser plug-in to assume responsibility for some rectangular region of the page, in which it can then display whatever it wants. In other words, plug-ins are less a matter of scripting the browser than of bypassing it entirely. Historically, plug-ins were widely used for content—animations and video in particular—that were poorly supported by HTML.

例 14.36

Example 14.36

在网页中嵌入小程序

Embedding an applet in a web page

设计为由 Java 插件运行的程序通常称为小程序。例如,考虑一个显示带有移动指针的时钟的小程序。旧版浏览器支持几种不同的小程序标签,但从 HTML5 开始,标准语法如下所示:

Programs designed to be run by a Java plug-in are commonly known as applets. Consider, for example, an applet to display a clock with moving hands.  Legacy browsers have supported several different applet tags, but as of HTML5 the standard syntax looks like this:

<嵌入类型=“application/x-java-applet” 代码=“Clock.class”>

<embed type=“application/x-java-applet” code=“Clock.class”>

type属性告知浏览器嵌入元素应为 Java 小程序;code元素提供小程序的 URI。可以使用其他属性来指定所需的解释器版本号和所需显示空间的大小等属性。

The type attribute informs the browser that the embedded element is expected to be a Java applet; the code element provides the applet's URI. Additional attributes can be used to specify such properties as the required interpreter version number and the size of the needed display space. ■

从type属性的存在可以推断,embed标签可以请求各种插件执行,而不仅仅是 Java 虚拟机。截至 2015 年,使用最广泛的插件是 Adob​​e 的 Flash Player。虽然可以编写脚本,与通用编程语言解释器相比,Flash Player 更准确地说是一个多媒体显示引擎。

As one might infer from the existence of the type attribute, embed tags can request execution by a wide variety of plug-ins—not just a Java Virtual Machine. As of 2015, the most widely used plug-in is Adobe's Flash Player. Though scriptable, Flash Player is more accurately described as a multimedia display engine than a general purpose programming language interpreter.

随着时间的推移,插件已被证明是浏览器安全漏洞的主要来源。几乎任何重要的插件都需要访问操作系统服务 — 网络 IO、本地文件空间、图形加速等等。提供刚好够用的服务 — 但又不至于造成任何危害 — 已被证明是极其困难的。为了解决这个问题,HTML5 标准中内置了广泛的多媒体支持,允许浏览器本身承担曾经通过插件完成的大部分工作。安全性仍然是一个问题,但必须信任的软件模块数量 — 以及攻击者可能试图进入的点数 — 已显著减少。许多浏览器现在默认禁用 Java。有些浏览器还禁用 Flash。

Over time, plug-ins have proven to be a major source of browser security bugs. Almost any nontrivial plug-in requires access to operating system services—network IO, local file space, graphics acceleration, and so on. Providing just enough service to make the plug-in useful—but not enough to allow it to do any harm—has proven extremely difficult. To address this problem, extensive multimedia support has been built into the HTML5 standard, allowing the browser itself to assume responsibility for much of what was once accomplished with plugins. Security is still a problem, but the number of software modules that must be trusted—and the number of points at which an attacker might try to gain entrance—is significantly reduced. Many browsers now disable Java by default. Some disable Flash as well.

14.3.5 XSLT

14.3.5 XSLT

毫无疑问,大多数读者都有机会编写或至少阅读用于编写网页的 HTML(超文本标记语言)。HTML 在大多数情况下都具有嵌套结构,其中文档片段(元素)由指示其用途或外观的标记分隔。例如,我们在14.2.2 节中看到,顶级标题用<h1></h1>分隔。不幸的是,由于 Web 的发展方式混乱且不规范,HTML 在设计上存在许多不一致之处,并且不同供应商实现的版本之间存在不兼容性。

Most readers will undoubtedly have had the opportunity to write, or at least to read, the HTML (hypertext markup language) used to compose web pages. HTML has, for the most part, a nested structure in which fragments of documents (elements) are delimited by tags that indicate their purpose or appearance. We saw in Section 14.2.2, for example, that top-level headings are delimited with <h1> and </h1>. Unfortunately, as a result of the chaotic and informal way in which the Web evolved, HTML ended up with many inconsistencies in its design, and incompatibilities among the versions implemented by different vendors.

设计与实现

Design & Implementation

14.5 JavaScript 和 Java

14.5 JavaScript and Java

尽管 JavaScript 有这个名字,但它与 Java 并无关联,除了表面上的语法相似性。该语言最初由 Brendan Eich 于 1995 年在 Netscape Corp. 开发。Eich 将他的发明称为LiveScript,但该公司在公开发布之前选择将其重命名,作为与 Sun Microsystems 达成的联合营销协议的一部分。JavaScript 名称的商标实际上归 Oracle 所有,后者于 2010 年收购了 Sun。

Despite its name, JavaScript has no connection to Java beyond some superficial syntactic similarity. The language was originally developed by Brendan Eich at Netscape Corp. in 1995. Eich called his creation LiveScript, but the company chose to rename it as part of a joint marketing agreement with Sun Microsystems, prior to its public release. Trademark on the JavaScript name is actually owned by Oracle, which acquired Sun in 2010.

1995 年,Netscape 浏览器成为市场领导者,JavaScript 的使用量增长极快。为了保持竞争力,微软的开发人员为 Internet Explorer 添加了 JavaScript 支持,但他们使用了JScript这个名称,这导致与 Netscape 版本的语言存在许多不兼容性。1997 年,欧洲标准机构将通用版本标准化为ECMAScript(随后 ISO 也将其标准化),但不同浏览器提供的文档对象模型仍然存在重大不兼容性。这些问题已通过万维网联盟的一系列标准逐渐得到解决,但遗留页面和遗留浏览器仍然困扰着 Web 开发人员。

Netscape's browser was the market leader in 1995, and JavaScript usage grew extremely fast. To remain competitive, developers at Microsoft added JavaScript support to Internet Explorer, but they used the name JScript instead, and they introduced a number of incompatibilities with the Netscape version of the language. A common version was standardized as ECMAScript by the European standards body in 1997 (and subsequently by the ISO), but major incompatibilities remained in the Document Object Models provided by different browsers. These have been gradually resolved through a series of standards from the World Wide Web Consortium, but legacy pages and legacy browsers continue to plague web developers.

XML(可扩展标记语言)是一种较新且通用的语言,可用于捕获结构化数据。与 HTML 相比,它的语法和语义更规则、更一致,并且在各个平台上的实现也更一致。它是可扩展的,这意味着用户可以定义自己的标签。它还明确区分了文档的内容(它捕获的数据)和数据的呈现。实际上,呈现被推迟到称为 XSL(可扩展样式表语言)的配套标准。XSLT 是 XSL 的一部分,专用于转换XML:选择、重新组织和修改标签及其分隔的元素 — 实际上,就是编写脚本来处理以 XML 表示的数据。

XML (extensible markup language) is a more recent and general language in which to capture structured data. Compared to HTML, its syntax and semantics are more regular and consistent, and more consistently implemented across platforms. It is extensible, meaning that users can define their own tags. It also makes a clear distinction between the content of a document (the data it captures) and the presentation of that data. Presentation, in fact, is deferred to a companion standard known as XSL (extensible stylesheet language). XSLT is a portion of XSL devoted to transforming XML: selecting, reorganizing, and modifying tags and the elements they delimit—in effect, scripting the processing of data represented in XML.

设计与实现

Design & Implementation

14.6 你能多大程度信任一个脚本?

14.6 How far can you trust a script?

每当使用他人的资源执行代码时,安全性就会成为一个问题。在托管机器上,Web 服务器通常安装时具有非常有限的访问权限,并且只能查看主机的文件系统。此策略将可通过服务器访问的页面集限制为直接登录到托管机器的用户可见的明确定义的子集。相比之下,CGI 脚本是单独的可执行程序,并且可能以安装它们的任何人的权限运行。为了防止托管机器上的用户意外或故意将其权限传递给 Internet 上的任意用户,大多数系统管理员会将他们的机器配置为 CGI 脚本必须驻留在特殊目录中,并由受信任的用户安装。嵌入式服务器端脚本可以驻留在任何文件中,因为它们保证以服务器本身的(有限)权限运行。

Security becomes an issue whenever code is executed using someone else's resources. On a hosting machine, web servers are usually installed with very limited access rights, and with only a limited view of the host's file system. This strategy limits the set of pages accessible through the server to a well-defined subset of what would be visible to users logged into the hosting machine directly. By contrast, CGI scripts are separate executable programs, and can potentially run with the privileges of whoever installs them. To prevent users on the hosting machine from accidentally or intentionally passing their privileges to arbitrary users on the Internet, most system administrators configure their machines so that CGI scripts must reside in a special directory, and be installed by a trusted user. Embedded server-side scripts can reside in any file because they are guaranteed to run with the (limited) rights of the server itself.

通过 Internet 下载并在客户端计算机上执行的代码会带来更大的风险。由于此类代码一般不受信任,因此必须在严格控制的环境(有时称为 沙箱 )中执行,防止其造成任何损害。一般而言,嵌入式 JavaScript 不能访问本地文件系统、内存管理系统或网络,也不能操作来自其他站点的文档。同样,Java 小程序访问外部资源的能力也有限。当然,实际情况要复杂一些:有时脚本需要访问有限大小的临时文件或与受信任服务器的网络连接。存在一些机制可以将站点认证为受信任的,或者允许受信任的站点认证来自其他站点的页面的可信度。通过受信任机制获得的页面上的脚本可能会被授予扩展权限。必须谨慎使用此类机制。在安全性和功能性之间找到适当的平衡仍然是 Web 以及分布式计算面临的主要挑战之一。 (有关该主题的更多信息,请参见第 16.2.4 节探索 16.1916.20 节。)

A larger risk is posed by code downloaded over the Internet and executed on a client machine. Because such code is in general untrusted, it must be executed in a carefully controlled environment, sometimes called a sandbox, to prevent it from doing any damage. As a general rule, embedded JavaScript cannot access the local file system, memory management system, or network, nor can it manipulate documents from other sites. Java applets, likewise, have only limited ability to access external resources. Reality is a bit more complicated, of course: Sometimes, a script needs access to, say, a temporary file of limited size, or a network connection to a trusted server. Mechanisms exist to certify sites as trusted, or to allow a trusted site to certify the trustworthiness of pages from other sites. Scripts on pages obtained through a trusted mechanism may then be given extended rights. Such mechanisms must be used with care. Finding the right balance between security and functionality remains one of the central challenges of the Web, and of distributed computing in general. (More on this topic can be found in Section 16.2.4, and in Explorations 16.19 and 16.20.)

14-02-9780124104099 更深入地

IN MORE DEPTH

XML 可用于为非常广泛的应用领域创建专用标记语言。XHTML 是符合 XML 标准的 HTML 的几乎(但不完全)向后兼容的变体。越来越多的 Web 工具被设计用于生成 XHTML。

XML can be used to create specialized markup languages for a very wide range of application domains. XHTML is an almost (but not quite) backward compatible variant of HTML that conforms to the XML standard. Web tools are increasingly being designed to generate XHTML.

在配套网站上,我们讨论了与 XML 相关的各种主题,并特别强调了 XSLT。我们详细阐述了内容和表示之间的区别,介绍了样式表语言的一般概念,并以XHTML 为例,描述了用于定义特定领域 XML 应用程序的文档类型定义(DTD) 和架构。

On the companion site, we consider a variety of topics related to XML, with a particular emphasis on XSLT. We elaborate on the distinction between content and presentation, introduce the general notion of stylesheet languages, and describe the document type definitions (DTDs) and schemas used to define domain-specific applications of XML, using XHTML as an example.

由于标签需要嵌套,因此 XML 文档具有自然的树状结构。XSLT 旨在通过递归遍历来处理这些树。虽然它可以用于几乎任何以 XML 作为输入的任务,但其最常见的用途可能是将 XML 转换为格式化的输出(通常是 XHTML)以在浏览器中显示。作为一个扩展示例,我们考虑基于 XML 的书目数据库的格式化。

Because tags are required to nest, an XML document has a natural tree-based structure. XSLT is designed to process these trees via recursive traversal. Though it can be used for almost any task that takes XML as input, perhaps its most common use is to transform XML into formatted output—often XHTML to be presented in a browser. As an extended example, we consider the formatting of an XML-based bibliographic database.

14-01-9780124104099检查你的理解

Check Your Understanding

23.解释 服务器端客户端Web 脚本之间的区别。

23. Explain the distinction between server-side and client-side web scripting.

24. 列出 CGI 脚本和嵌入式 PHP 之间的权衡。

24. List the tradeoffs between CGI scripts and embedded PHP.

25. 为什么 CGI 脚本通常只安装在特殊的目录中?

25. Why are CGI scripts usually installed only in a special directory?

26. 解释 PHP 页面如何满足其自身请求。

26. Explain how a PHP page can service its own requests.

27. 为什么我们更喜欢在服务器上而不是在客户端上执行 Web 脚本?为什么我们有时更喜欢在客户端上执行?

27. Why might we prefer to execute a web script on the server rather than the client? Why might we sometimes prefer the client instead?

28. 什么是 HTML文档对象模型? 它对于客户端脚本有何意义?

28. What is the HTML Document Object Model? What is its significance for client-side scripting?

29.JavaScript 和Java有什么关系?

29. What is the relationship between JavaScript and Java?

30. 什么是小程序?为什么小程序通常不被视为脚本的示例?

30. What is an applet? Why applets are usually not considered an example of scripting?

31. 什么是 HTML? XML? XSLT? 它们之间有什么关系?

31. What is HTML? XML? XSLT? How are they related to one another?

14.4 创新功能

14.4 Innovative Features

14.1.1 节中,我们列出了脚本语言的几个共同特征:

In Section 14.1.1, we listed several common characteristics of scripting languages:

1. 批量和交互使用

1. Both batch and interactive use

2. 表达经济

2. Economy of expression

3. 缺乏声明;作用域规则简单

3. Lack of declarations; simple scoping rules

4. 灵活的动态类型

4. Flexible dynamic typing

5. 轻松访问其他程序

5. Easy access to other programs

6. 复杂的模式匹配和字符串操作

6. Sophisticated pattern matching and string manipulation

7. 高级数据类型

7. High-level data types

以下小节将更详细地讨论其中的几个。具体来说,第 14.4.1 节讨论了脚本语言中的命名和作用域;第 14.4.2 节讨论了字符串和模式操作;第 14.4.3 节讨论了数据类型。我们列表中的项目 (1)、(2) 和 (5) 虽然很重要,但并不是特别困难或微妙,因此这里不再赘述。

Several of these are discussed in more detail in the subsections below. Specifically, Section 14.4.1 considers naming and scoping in scripting languages; Section 14.4.2 discusses string and pattern manipulation; and Section 14.4.3 considers data types. Items (1), (2), and (5) in our list, while important, are not particularly difficult or subtle, and will not be considered further here.

14.4.1 名称和范围

14.4.1 Names and Scopes

大多数脚本语言(Scheme 是明显的例外)不需要声明变量。少数语言(尤其是 Perl 和 JavaScript)允许可选声明,主要作为一种编译器检查文档。Perl 可以在需要声明的模式下运行(使用严格的“vars”)。无论有没有声明,大多数脚本语言都使用动态类型。值通常是自描述的,因此解释器可以在运行时执行类型检查,或在适当的时候强制值。

Most scripting languages (Scheme is the obvious exception) do not require variables to be declared. A few languages, notably Perl and JavaScript, permit optional declarations, primarily as a sort of compiler-checked documentation. Perl can be run in a mode (use strict ' vars') that requires declarations. With or without declarations, most scripting languages use dynamic typing. Values are generally self-descriptive, so the interpreter can perform type checking at run time, or coerce values when appropriate.

嵌套和作用域约定差异很大。Scheme、Python、JavaScript 和 R 提供了嵌套子程序和静态(词法)作用域的经典组合。Tcl 允许子程序嵌套,但使用动态作用域。命名子程序(方法)在 PHP 或 Ruby 中不嵌套,在 Perl 中也只是某种程度上的嵌套(下面还会详细介绍),但 Perl 和 Ruby 与 Scheme、Python、JavaScript 和 R 一起提供了一流的匿名本地子程序。嵌套块在 Perl 中是静态作用域。在 Ruby 中,它们是它们出现的命名作用域的一部分。Scheme、Perl、Python、Ruby、JavaScript 和 R 都为闭包中捕获的变量提供了无限范围。PHP、R 和主要的粘合语言(Perl、Tcl、Python、Ruby)都具有复杂的命名空间机制,用于信息隐藏和从单独的模块选择性导入名称。

Nesting and scoping conventions vary quite a bit. Scheme, Python, JavaScript, and R provide the classic combination of nested subroutines and static (lexical) scope. Tcl allows subroutines to nest, but uses dynamic scoping. Named subroutines (methods) do not nest in PHP or Ruby, and they are only sort of nest in Perl (more on this below as well), but Perl and Ruby join Scheme, Python, JavaScript, and R in providing first-class anonymous local subroutines. Nested blocks are statically scoped in Perl. In Ruby, they are part of the named scope in which they appear. Scheme, Perl, Python, Ruby, JavaScript, and R all provide unlimited extent for variables captured in closures. PHP, R, and the major glue languages (Perl, Tcl, Python, Ruby) all have sophisticated namespace mechanisms for information hiding and the selective import of names from separate modules.

未声明的变量的范围是什么?

What Is the Scope of an Undeclared Variable?

在具有静态作用域的语言中,缺少声明引发了一个有趣的问题:当我们访问变量x时,我们如何知道它是本地的、全局的,还是(如果作用域可以嵌套)介于两者之间的某个变量?现有的语言采用了几种不同的方法。在 Perl 中,除非明确声明,否则所有变量都是全局的。在 PHP 中,除非明确导入,否则它们是本地的(并且所有导入都是全局的,因为作用域不嵌套)。Ruby 也只有两个实际的作用域级别,但正如我们在14.2.4 节中看到的,它使用名称上的前缀字符来区分它们:foo是本地变量;$foo是全局变量;@foo是当前对象(其方法当前正在执行的对象)的实例变量;@@foo是当前对象的实例变量(由所有兄弟实例共享)。(注意:正如我们将在14.4.3 节中看到的,Perl 使用类似的前缀字符来指示类型。这些非常不同的用法可能会让在两种语言之间切换的程序员感到困惑。)

In languages with static scoping, the lack of declarations raises an interesting question: when we access a variable x, how do we know if it is local, global, or (if scopes can nest) something in-between? Existing languages take several different approaches. In Perl, all variables are global unless explicitly declared. In PHP, they are local unless explicitly imported (and all imports are global, since scopes do not nest). Ruby, too, has only two real levels of scoping, but as we saw in Section 14.2.4, it distinguishes between them using prefix characters on names: foo is a local variable; $foo is a global variable; @foo is an instance variable of the current object (the one whose method is currently executing); @@foo is an instance variable of the current object's class (shared by all sibling instances). (Note: as we shall see in Section 14.4.3, Perl uses similar prefix characters to indicate type. These very different uses are a potential source of confusion for programmers who switch between the two languages.)

例 14.37

Example 14.37

Python 中的作用域规则

Scoping rules in Python

也许最有趣的作用域解析规则是 Python 和 R 的规则。在这些语言中,除非明确导入,否则写入的变量都被视为本地变量。只能在给定作用域中读取的变量位于包含定义写入的最近封闭作用域中。例如,考虑图 14.16中的 Python 程序。这里我们有一组嵌套的子例程,如缩进级别所示。主程序调用outer,后者调用middle,后者又调用inner。在调用之前,主程序写入ij。Outer读取j(将其传递给middle ),但写入它。但是,它写入i。因此,outer读取全局的j,但有自己的i,与全局的 i 不同。Middle读取ij,但都不写入,因此它必须在周围作用域中找到它们。它在outer中找到i,在全局级别找到j。就 Inner 而言,它还写入全局的i。执行时,程序会打印

Perhaps the most interesting scope-resolution rule is that of Python and R. In these languages, a variable that is written is assumed to be local, unless it is explicitly imported. A variable that is only read in a given scope is found in the closest enclosing scope that contains a defining write. Consider, for example, the Python program of Figure 14.16. Here we have a set of nested subroutines, as indicated by indentation level. The main program calls outer, which calls middle, which in turn calls inner. Before its call, the main program writes both i and j. Outer reads j (to pass it to middle), but does not write it. It does, however, write i. Consequently, outer reads the global j, but has its own i, different from the global one. Middle reads both i and j, but it does not write either, so it must find them in surrounding scopes. It finds i in outer, and j at the global level. Inner, for its part, also writes the global i. When executed, the program prints

f14-16-9780124104099
图 14.16 一个用于说明 Python 中的作用域规则的程序。jk各有一个实例,但i两个实例:一个是全局实例,一个是outer的局部实例。后者的作用域是outer 的全部,而不仅仅是赋值后的部分。global 语句为inner提供了对最外层i 的访问权限,因此它可以在不定义新实例的情况下写入它。

(2、3、3)

(2, 3, 3)

4 3

4 3

请注意,虽然middle返回的元组(由outer转发,并由主程序打印)的第一个元素为2,但全局i仍包含inner写入的 4。还请注意,虽然outer中对i的写入在middle中对i的读取之后以文本形式出现,但其范围扩展到outer 的整个部分,包括middle的主体。■

Note that while the tuple returned from middle (forwarded on by outer, and printed by the main program) has a 2 as its first element, the global i still contains the 4 that was written by inner. Note also that while the write to i in outer appears textually after the read of i in middle, its scope extends over all of outer, including the body of middle. ■

例 14.38

Example 14.38

R 中的超级分配

Superassignment in R

有趣的是,在 Python 中,嵌套例程无法写入属于周围但非全局作用域的变量。在图 14.16中,不能修改inner来写入outeri。R 提供了一种提供此功能的替代机制。R 不会将i声明为全局的,而是使用“超级赋值”运算符。普通赋值i <- 4将值 4 赋给局部变量i,而超级赋值i <<- 4会将 4 赋给在静态(词法)作用域的正常规则下可以找到的任何i 。■

Interestingly, there is no way in Python for a nested routine to write a variable that belongs to a surrounding but nonglobal scope. In Figure 14.16, inner could not be modified to write outer's i. R provides an alternative mechanism that does provide this functionality. Rather than declare i to be global, R uses a “superassignment” operator. Where a normal assignment i <- 4 assigns the value 4 into a local variable i, the superassignment i <<- 4 assigns 4 into whatever i would be found under the normal rules of static (lexical) scoping. ■

Perl 中的作用域

Scoping in Perl

Perl 多年来一直在发展。起初,只有全局变量。为了模块化,很快就添加了局部变量,因此带有名为i的变量的子例程不必担心修改代码中其他地方需要的全局i。不幸的是,局部变量最初是根据动态作用域定义的,而向后兼容的需要要求在 Perl 5 中添加静态作用域时保留此行为。因此,该语言提供了这两种机制。

Perl has evolved over the years. At first, there were only global variables. Locals were soon added for the sake of modularity, so a subroutine with a variable named i wouldn't have to worry about modifying a global i that was needed elsewhere in the code. Unfortunately, locals were originally defined in terms of dynamic scoping, and the need for backward compatibility required that this behavior be retained when static scoping was added in Perl 5. Consequently, the language provides both mechanisms.

例 14.39

Example 14.39

Perl 中的静态和动态作用域

Static and dynamic scoping in Perl

默认情况下,任何未声明的变量在 Perl 中都是全局的。使用local运算符声明的变量具有动态作用域。使用my运算符声明的变量具有静态作用域。图 14.17中可以看到两者的区别,其中子例程outer声明了两个局部变量lexdyn。前者具有静态作用域,后者具有动态作用域。两者都被初始化为foo的第一个参数的副本。(参数在伪变量@_中传递。此数组的第一个元素是$_[0]。)

Any variable that is not declared is global in Perl by default. Variables declared with the local operator are dynamically scoped. Variables declared with the my operator are statically scoped. The difference can be seen in Figure 14.17, in which subroutine outer declares two local variables, lex and dyn. The former is statically scoped; the latter is dynamically scoped. Both are initialized to be a copy of foo's first parameter. (Parameters are passed in the pseudovariable @_. The first element of this array is $_[0].)

f14-17-9780124104099
图 14.17 一个用于说明 Perl 中的作用域规则的程序。my运算符创建一个静态作用域局部变量;local 运算符创建一个新的动态作用域全局变量实例。静态作用域从声明点延伸到块的词法结尾;动态作用域从阐述延伸到块执行的结尾。

outer中嵌套了两个词法相同的匿名子例程,一个位于$lex$dyn的重新声明之前,一个位于其之后。对这些子例程的引用如下:存储在局部变量sub_Asub_B 中。由于 Perl 中的静态作用域从声明延伸到其块的末尾,因此sub_A看到的是全局$lex,sub_B看到的是outer$lex。相反,由于局部 $dyn的声明发生在调用sub_Asub_B之前,因此两者都看到的是这个局部版本。我们的程序打印

Two lexically identical anonymous subroutines are nested inside outer, one before and one after the redeclarations of $lex and $dyn. References to these are stored in local variables sub_A and sub_B. Because static scopes in Perl extend from a declaration to the end of its block, sub_A sees the global $lex, while sub_B sees outer's $lex. In contrast, because the declaration of local $dyn occurs before either sub_A or sub_B is called, both see this local version. Our program prints

主要 1, 1

main 1, 1

外 2, 2

outer 2, 2

sub_A 1, 2

sub_A 1, 2

sub_B 2, 2

sub_B 2, 2

主页 1, 1

main 1, 1

例 14.40

Example 14.40

在 Perl 中访问全局变量

Accessing globals in Perl

在静态作用域通常会访问嵌套级别之间的变量的情况下,Perl 允许程序员通过 our运算符强制使用全局变量,其名称旨在与my 形成对比:

In cases where static scoping would normally access a variable at an in-between level of nesting, Perl allows the programmer to force the use of a global variable with the our operator, whose name is intended to contrast with my:

($x, $y, $z) = (1, 1, 1); # 全局范围

($x, $y, $z) = (1, 1, 1); # global scope

{ # 中间范围

{ # middle scope

 我的($x,$y)=(2,2);

 my ($x, $y) = (2, 2);

 本地$z = 3;

 local $z = 3;

 { # 内部范围

 { # inner scope

  our ($x, $z); # 使用全局变量

  our ($x, $z); # use globals

  打印“$x,$y,$z\n”;

  print “$x, $y, $z\n”;

 }

 }

}

}

这里有一个z的词法实例,以及两个xy的词法实例——一个是全局的,一个在中间作用域中。中间作用域中还有一个动态z 。当它执行时在其打印语句中,内部作用域从中间作用域找到y 。但是,由于第 6 行的our运算符,它找到了全局的x 。现在z怎么样?规则要求我们从静态作用域开始,忽略局部运算符。那么,根据内部作用域中的our运算符,我们使用的是全局的z 。一旦我们知道了这一点,我们就会查看z的动态(局部)重新声明是否有效。在本例中确实有效,我们的程序打印1, 2, 3。事实证明,内部作用域中的our声明对这个程序没有影响。如果x被声明为 our,我们仍然会使用全局的z,然后从中间作用域找到动态实例。■

Here there is one lexical instance of z and two of x and y—one global, one in the middle scope. There is also a dynamic z in the middle scope. When it executes its print statement, the inner scope finds the y from the middle scope. It finds the global x, however, because of the our operator on line 6. Now what about z? The rules require us to start with static scoping, ignoring local operators. According, then, to the our operator in the inner scope, we are using the global z. Once we know this, we look to see whether a dynamic (local) redeclaration of z is in effect. In this case indeed it is, and our program prints 1, 2, 3. As it turns out, the our declaration in the inner scope had no effect on this program. If only x had been declared our, we would still have used the global z, and then found the dynamic instance from the middle scope. ■

设计与实现

Design & Implementation

14.7 关于动态作用域的思考

14.7 Thinking about dynamic scoping

3.3.6 节中,我们描述了动态作用域规则,即为名称引入新的含义,该含义在程序中的任何地方都可见,直到控制权离开创建新含义的作用域。此概念模型反映了 C-3.4.2 节中描述的关联列表实现,并且如边栏 3.6 中所述,可能解释了 Lisp 早期方言中使用动态作用域的原因。

In Section 3.3.6, we described dynamic scope rules as introducing a new meaning for a name that remains visible, wherever we are in the program, until control leaves the scope in which the new meaning was created. This conceptual model mirrors the association list implementation described in Section C-3.4.2 and, as described in Sidebar 3.6, probably accounts for the use of dynamic scoping in early dialects of Lisp.

Perl 的文档建议使用语义上等同但概念上不同的模型。Perl 不会说局部声明会引入一个新变量,其名称会隐藏先前的声明,而是说在全局级别上存在一个变量,当遇到新声明时,会保存其先前的,然后在控制权离开新声明的范围时自动恢复。此模型反映了 Perl 中的底层实现,它使用中央参考表(也在 C-3.4.2 节中描述)。为了与此模型和实现保持一致,Perl 不允许局部运算符创建非全局变量的动态实例。

Documentation for Perl suggests a semantically equivalent but conceptually different model. Rather than saying that a local declaration introduces a new variable whose name hides previous declarations, Perl says that there is a single variable, at the global level, whose previous value is saved when the new declaration is encountered, and then automatically restored when control leaves the new declaration's scope. This model mirrors the underlying implementation in Perl, which uses a central reference table (also described in Section C-3.4.2). In keeping with this model and implementation, Perl does not allow a local operator to create a dynamic instance of a variable that is not global.

14.4.2 字符串和模式操作

14.4.2 String and Pattern Manipulation

当我们在2.1.1 节中第一次考虑正则表达式时,我们注意到许多脚本语言和相关工具都使用了该符号的扩展版本。一些扩展只是为了方便。其他扩展则增加了符号的表达能力,使我们能够生成(匹配)非正则字符串集。还有一些扩展用于将符号与其他语言特性联系起来。

When we first considered regular expressions, in Section 2.1.1, we noted that many scripting languages and related tools employ extended versions of the notation. Some extensions are simply a matter of convenience. Others increase the expressive power of the notation, allowing us to generate (match) nonregular sets of strings. Still other extensions serve to tie the notation to other language features.

我们已经在sed图 14.1)、awk图 14.214.3)、Perl(图 14.414.5)、Python(图 14.6)和 Ruby(图 14.7)中看到了扩展正则表达式的示例。许多读者也熟悉grep,它是独立的 Unix 模式匹配工具(参见边栏 14.8)。

We have already seen examples of extended regular expressions in sed (Figure 14.1), awk (Figures 14.2 and 14.3), Perl (Figures 14.4 and 14.5), Python (Figure 14.6), and Ruby (Figure 14.7). Many readers will also be familiar with grep, the stand-alone Unix pattern-matching tool (see Sidebar 14.8).

扩展正则表达式 (简称“RE”) 有很多不同的实现,而且语法略有不同,但大多数都分为两大类。第一类包括awk、egrep (在grep的几个不同版本中使用最广泛的)和C 的regex库。它们实现了 POSIX 标准 [ Int03b ] 中定义的正则表达式。第二类语言以 Perl 为榜样,Perl 提供了一大套扩展,有时被称为“高级正则表达式”。类似 Perl 的高级正则表达式出现在 PHP、Python、Ruby、JavaScript、Emacs Lisp、Java 和 C# 中。在 C++ 和其他语言的第三方软件包中也可以找到它们。一些工具,包括sed、经典grep和较旧的 Unix 编辑器,提供所谓的“基本”正则表达式,功能不如egrep 的正则表达式。

While there are many different implementations of extended regular expressions (“REs” for short), with slightly different syntax, most fall into two main groups. The first group includes awk, egrep (the most widely used of several different versions of grep), and the regex library for C. These implement REs as defined in the POSIX standard [Int03b]. Languages in the second group follow the lead of Perl, which provides a large set of extensions, sometimes referred to as “advanced REs.” Perl-like advanced REs appear in PHP, Python, Ruby, JavaScript, Emacs Lisp, Java, and C#. They can also be found in third-party packages for C++ and other languages. A few tools, including sed, classic grep, and older Unix editors, provide so-called “basic” REs, less capable than those of egrep.

在某些语言和工具中(尤其是sedawk、Perl、PHP、Ruby 和 JavaScript),正则表达式与语言的其余部分紧密集成,具有特殊语法和内置运算符。在这些语言中,正则表达式通常用斜杠字符分隔,但在某些情况下也可以接受其他分隔符(事实上,Perl 为一些备选分隔符提供了略有不同的语义)。在大多数其他语言中,正则表达式表示为普通字符串,并通过将它们传递给库例程来操作。在接下来的几页中,我们将更详细地考虑 POSIX 和高级正则表达式。在介绍 Perl 之后,我们将使用斜杠作为分隔符。我们的介绍必然是不完整的。Perl 一书中有关正则表达式的章节 [ CfWO12第 5 章] 超过 100 页。相应的 Unix手册页总共大约 40 页。

In certain languages and tools—notably sed, awk, Perl, PHP, Ruby, and JavaScript—regular expressions are tightly integrated into the rest of the language, with special syntax and built-in operators. In these languages an RE is typically delimited with slash characters, though other delimiters maybe accepted in some cases (and Perl in fact provides slightly different semantics for a few alternative delimiters). In most other languages, REs are expressed as ordinary character strings, and are manipulated by passing them to library routines. Over the next few pages we will consider POSIX and advanced REs in more detail. Following Perl, we will use slashes as delimiters. Our coverage will of necessity be incomplete. The chapter on REs in the Perl book [CfWO12, Chap. 5] is over 100 pages long. The corresponding Unix man page totals some 40 pages.

POSIX 正则表达式

POSIX Regular Expressions

例 14.41

Example 14.41

POSIX Res 中的基本操作

Basic operations in POSIX Res

与形式语言理论的“真正”正则表达式一样,扩展正则表达式支持连接、交替和 Kleene 闭包。括号用于分组:

Like the “true” regular expressions of formal language theory, extended REs support concatenation, alternation, and Kleene closure. Parentheses are used for grouping:

/ab(cd|ef)g*/ 匹配 abcd、abcdg、abefg、abefgg、abcdggg 等。

/ab(cd|ef)g*/    matches abcd, abcdg, abefg, abefgg, abcdggg, etc.

例 14.42

Example 14.42

POSIX Res 中的额外量词

Extra quantifiers in POSIX Res

还有几个其他量词(克莱尼闭包的推广): ? 表示零次或一次重复, + 表示一次或多次重复, { n } 表示恰好n 次重复, { n ,} 表示至少n次重复,而 { n , m } 表示nm 次重复:

Several other quantifiers (generalizations of Kleene closure) are also available: ? indicates zero or one repetitions, + indicates one or more repetitions, {n} indicates exactly n repetitions, {n,} indicates at least n repetitions, and {n, m} indicates n-m repetitions:

/a(bc)*/匹配 a、abc、abcbc、abcbcbc 等。
/a(bc)?/匹配 a 或 abc
/a(bc)+/匹配 abc、abcbc、abcbcbc 等。
/a(bc){3}/仅匹配 abcbcbc
/a(bc){2,}/匹配 abcbc、abcbcbc 等等。
/a(bc){1,3}/匹配 abc、abcbc 和 abcbcbc(仅限)

例 14.43

Example 14.43

零长度断言

Zero-length assertions

两个零长度断言^ 和 $ 分别仅匹配目标字符串的开头和结尾。因此,虽然 / abe / 将匹配abeabetbabelabel,但 /^ abe / 将仅匹配其中的前两个, / abe$ / 将仅匹配第一个和第三个,而 /^ abe $/ 将仅匹配第一个。■

Two zero-length assertions, ^ and $, match only at the beginning and end, respectively, of a target string. Thus while /abe/ will match abe, abet, babe, and label, /^abe/ will match only the first two of these, /abe$/ will match only the first and the third, and /^abe$/ will match only the first. ■

例 14.44

Example 14.44

字符类

Character classes

作为单个字符集交替的缩写(例如,/ a|e|i|o|u /),扩展 RE 允许使用方括号指定字符类:

As an abbreviation for the alternation of set of single characters (e.g., /a|e|i|o|u/), extended REs permit character classes to be specified with square brackets:

/b[aeiou]d/ 匹配 bad、bed、bid、bo 和 bud

/b[aeiou]d/ matches bad, bed, bid, bod, and bud

设计与实现

Design & Implementation

14.8 grep命令和 Unix 工具的诞生

14.8 The grep command and the birth of Unix tools

从历史上看,正则表达式工具源于ed行编辑器的模式匹配机制,该机制可追溯到 Unix 诞生之初。1973 年,Unix 诞生部门的负责人 Doug McIlroy 当时正在从事一个计算机语音合成项目。作为该项目的一部分,他使用该编辑器在在线词典中搜索可能具有挑战性的单词。这个过程既乏味又缓慢。应 McIlroy 的要求,Ken Thompson 从ed中提取了模式匹配器,并将其作为一个独立工具。他根据编辑器中的g/re/p命令序列将自己的作品命名为grep : g代表“全局”;/ / 搜索正则表达式(re);p打印[ HH97a第 9 章]。

Historically, regular expression tools have their roots in the pattern matching mechanism of the ed line editor, which dates from the earliest days of Unix. In 1973, Doug McIlroy, head of the department where Unix was born, was working on a project in computerized voice synthesis. As part of this project he was using the editor to search for potentially challenging words in an online dictionary. The process was both tedious and slow. At McIlroy's request, Ken Thompson extracted the pattern matcher from ed and made it a stand-alone tool. He named his creation grep, after the g/re/p command sequence in the editor: g for “global”; / / to search for a regular expression (re); p to print [HH97a, Chap. 9].

Thompson 的创作是第一批基于流的 Unix 工具之一。如第 14.2.1 节所述,此类工具经常与管道结合使用,以执行各种过滤、转换和格式化操作。

Thompson's creation was one of the first in a large suite of stream-based Unix tools. As described in Section 14.2.1, such tools are frequently combined with pipes to perform a variety of filtering, transforming, and formatting operations.

范围也是允许的:

Ranges are also permitted:

/0x[0-9a-fA-F]+/ 匹配任意十六进制整数

/0x[0-9a-fA-F]+/ matches any hexadecimal integer

例 14.45

Example 14.45

点 (.) 字符

The dot (.) character

在字符类之外,点 (.) 可匹配除换行符之外的任何字符。例如,表达式 / bd / 不仅可匹配bad、bbd、bcd等,还可匹配b:d、b7d和许多其他字符,包括中间字符不可打印的序列。在支持 Unicode 的 Perl 版本中,有数以万计的选项。■

Outside a character class, a dot (.) matches any character other than a newline. The expression /b.d/, for example, matches not only bad, bbd, bcd, and so on, but also b:d, b7d, and many, many others, including sequences in which the middle character isn't printable. In a Unicode-enabled version of Perl, there are tens of thousands of options. ■

例 14.46

Example 14.46

字符类中的否定和引用

Negation and quoting in character classes

字符类开头的插入符号 (^) 表示否定:类表达式匹配除内部字符之外的所有内容。因此, /b[^aq]d/匹配除badbqd之外的由/bd/匹配的所有内容。插入符号、右括号或连字符可以在字符类内部指定,方法是在其前面加上反斜杠。反斜杠同样可以保护字符类外部的任何特殊字符| ( ) [ ]{}$.* + ? 。6要匹配文字反斜杠,请连续使用两个反斜杠:

A caret (^) at the beginning of a character class indicates negation: the class expression matches anything other than the characters inside. Thus /b[^aq]d/ matches anything matched by /b.d/ except for bad and bqd. A caret, right bracket, or hyphen can be specified inside a character class by preceding it with a backslash. A backslash will similarly protect any of the special characters | ( ) [ ]{}$.* + ? outside a character class.6 To match a literal backslash, use two of them in a row:

/a\\b/ 匹配 a\b

/a\\b/ matches a\b

例 14.47

Example 14.47

预定义 POSIX 字符类

Predefined POSIX character classes

POSIX 标准预定义了几个字符类表达式。如示例 14.19所示,表达式[:space:]可用于捕获空格。标点符号为[:punct:]。这些类的确切定义取决于本地字符集和语言。还要注意,这些表达式必须内置字符类中使用;它们本身并不是类。例如,C 中的变量名可能与/[[:alpha:]_] [[:alpha:][:digit :]_]*/匹配,或者更简单地说,与/[[:alpha:]_][[:alnum :]_]*/匹配。其他语法(未在此处描述)允许字符类捕获 Unicode排序元素(多字节序列,例如字符和相关重音符),这些元素像单个元素一样进行排序(排序)。Perl 提供了大多数这些特殊类的更简洁的版本。■

Several character class expressions are predefined in the POSIX standard. As we saw in Example 14.19, the expression [:space:] can be used to capture white space. For punctuation there is [:punct:]. The exact definitions of these classes depend on the local character set and language. Note, too, that the expressions must be used inside a built-up character class; they aren't classes by themselves. A variable name in C, for example, might be matched by /[[:alpha:]_] [[:alpha:][:digit :]_]*/ or, a bit more simply, /[[:alpha:]_][[:alnum :]_]*/. Additional syntax, not described here, allows character classes to capture Unicode collating elements (multibyte sequences such as a character and associated accents) that collate (sort) as if they were single elements. Perl provides less cumbersome versions of most of these special classes. ■

Perl 扩展

Perl Extensions

例 14.48

Example 14.48

Perl 中的 RE 匹配

RE matching in Perl

扩展正则表达式是 Perl 的核心部分。内置的 =~ 运算符用于测试匹配:

Extended REs are a central part of Perl. The built-in =~ operator is used to test for matching:

$foo =“信天翁”;
如果 ($foo =~ /ba.*s+/) …# 真的
如果 ($foo =~ /^ba.*s+/) …# false (字符串开头无匹配)

还可以不指定要匹配的字符串,这种情况下 Perl 默认使用伪变量 $_:

The string to be matched against can also be left unspecified, in which case Perl uses the pseudovariable $_ by default:

$_ =“信天翁”;
如果(/ba.*s+/)…# 真的
如果 (/^ba.*s+/) …# 错误的

回想一下,在迭代文件的行时会自动设置$_ 。它也是for循环中的默认索引变量。■

Recall that $_ is set automatically when iterating over the lines of a file. It is also the default index variable in for loops. ■

例 14.49

Example 14.49

在 Perl 中否定匹配

Negating a match in Perl

当模式匹配时, !~ 运算符返回 true:

The !~ operator returns true when a pattern does not match:

如果(“信天翁” !~ /^ba.*s+/)… # true

if (“albatross” !~ /^ba.*s+/) … # true

例 14.50

Example 14.50

Perl 中的 RE 替换

RE substitution in Perl

对于替换,二进制“混合”运算符s///将第一个和第二个斜杠之间的内容替换为第二个和第三个斜杠之间的内容:

For substitution, the binary “mixfix” operator s/// replaces whatever lies between the first and second slashes with whatever lies between the second and the third:

$foo =“信天翁”;
$foo =〜s / lbat / c /;# “穿过”

同样,如果左侧未指定,则s///匹配并修改$_。■

Again, if a left-hand side is not specified, s/// matches and modifies $_. ■

修饰符和转义序列

Modifiers and Escape Sequences

例 14.51

Example 14.51

RE 匹配中的尾随修饰符

Trailing modifiers on RE matches

通过在结束分隔符后添加一个或多个字符,可以修改匹配和替换。例如,尾随的i使匹配不区分大小写:

Both matches and substitutions can be modified by adding one or more characters after the closing delimiter. A trailing i, for example, makes the match case-insensitive:

$foo =“信天翁”;
如果 ($foo =~ /^al/i) …# 真的

替换中的尾随g将替换所有出现的正则表达式:

A trailing g on a substitution replaces all occurrences of the regular expression:

$foo =“信天翁”;
$foo =~ s/[aeiou]/-/g;#“-lb-tr-ss” 复制代码

设计与实现

Design & Implementation

14.9 正则表达式的自动机

14.9 Automata for regular expressions

POSIX 正则表达式通常使用第 2.2.1 节中描述的构造来实现,该构造将 RE 转换为 NFA,然后再转换为 DFA。Perl 提供的高级 RE 通常通过明显的 NFA 中的回溯搜索来实现。通常不采用 NFA 到 DFA 的构造,因为它无法保留某些高级 RE 扩展(特别是示例 14.5514.58中描述的捕获机制)[ CfWO12,第 241–246 页]。某些实现首先使用 DFA 来确定是否存在匹配,然后使用 NFA 或回溯搜索来实际实现匹配。只有在确信值得时,此策略才会付出较慢自动机的代价。

POSIX regular expressions are typically implemented using the constructions described in Section 2.2.1, which transform the RE into an NFA and then a DFA. Advanced REs of the sort provided by Perl are typically implemented via backtracking search in the obvious NFA. The NFA-to-DFA construction is usually not employed because it fails to preserve some of the advanced RE extensions (notably the capture mechanism described in Examples 14.5514.58) [CfWO12, pp. 241–246]. Some implementations use a DFA first to determine whether there is a match, and then an NFA or backtracking search to actually effect the match. This strategy pays the price of the slower automaton only when it's sure to be worthwhile.

对于多行字符串中的匹配,尾随的s允许点 (.) 匹配嵌入的换行符(通常无法匹配)。尾随的m允许$^分别匹配此类换行符之前和之后的换行符。尾随的x使 Perl 忽略模式中的注释和嵌入的空格,这样可以将特别复杂的表达式拆分为多行、记录和缩进。

For matching in multiline strings, a trailing s allows a dot (.) to match an embedded newline (which it normally cannot). A trailing m allows $ and ^ to match immediately before and after such a newline, respectively. A trailing x causes Perl to ignore both comments and embedded white space in the pattern so that particularly complicated expressions can be broken across multiple lines, documented, and indented.

按照 C 及其相关语言(示例 8.29 )的传统,Perl 允许使用反斜杠转义序列在 RE 中指定非打印字符。一些最常用的示例出现在图 14.18的上部。除了标准的 ^ 和 $ 之外,Perl 还提供了几个零宽度断言。示例显示在图的中间。\A\Z转义与 ^ 和 $ 不同,因为它们分别只在字符串的开头和结尾继续匹配,即使在使用修饰符m 的多行搜索中也是如此。最后,Perl 提供了几个内置字符类,其中一些显示在图的底部。这些可以在用户定义(即括号分隔)类的内部和外部使用。请注意,\ b在这些类内部和外部具有不同的含义。

In the tradition of C and its relatives (Example 8.29), Perl allows nonprinting characters to be specified in REs using backslash escape sequences. Some of the most frequently used examples appear in the top portion of Figure 14.18. Perl also provides several zero-width assertions, in addition to the standard ^ and $. Examples are shown in the middle of the figure. The \A and \Z escapes differ from ^ and $ in that they continue to match only at the beginning and end of the string, respectively, even in multiline searches that use the modifier m. Finally, Perl provides several built-in character classes, some of which are shown at the bottom of the figure. These can be used both inside and outside user-defined (i.e., bracket-delimited) classes. Note that \b has different meanings inside and outside such classes.

f14-18-9780124104099
图 14.18 Perl 中的正则表达式转义序列。表格顶部的序列代表单个字符。中间的序列是零宽度断言。底部的序列是内置字符类。请注意,这些只是示例:Perl 为几乎每个反斜杠字符序列分配了一个含义。

贪婪匹配和最小匹配

Greedy and Minimal Matches

例 14.52

Example 14.52

贪婪和最小匹配

Greedy and minimal matching

正则表达式中的匹配规则有时称为“最左最长”:当模式可以在字符串中的多个位置匹配时,所选匹配将从字符串中最早可能的位置开始,然后尽可能延伸。例如,在字符串 abcbcbcde 中模式/(bc)+/可以以六种不同的方式匹配:

The usual rule for matching in REs is sometimes called “left-most longest”: when a pattern can match at more than one place within a string, the chosen match will be the one that starts at the earliest possible position within the string, and then extends as far as possible. In the string abcbcbcde, for example, the pattern /(bc)+/ can match in six different ways:

bc bcbcde

abcbcbcde

bcbc bcde

abcbcbcde

bcbcbc德

abcbcbcde

ABC BC BCDE

abcbcbcde

abc bcbc

abcbcbcde

abcbc bc de

abcbcbcde

其中第三种是“最左最长”,也称为贪婪。但在某些情况下,可能需要获得“最左最短”或最小匹配。这对应于上面的第一种选择。■

The third of these is “left-most longest,” also known as greedy. In some cases, however, it may be desirable to obtain a “left-most shortest” or minimal match. This corresponds to the first alternative above. ■

例 14.53

Example 14.53

HTML 标头的最小匹配

Minimal matching of HTML headers

我们在示例 14.23图 14.4 )中看到一个更现实的例子,其中包含以下替换:

We saw a more realistic example in Example 14.23 (Figure 14.4), which contains the following substitution:

s/.*?(<[hH][123]>.*?<\/[hH][123]>)//s;

s/.*?(<[hH][123]>.*?<\/[hH][123]>)//s;

假设 HTML 输入格式正确,并且标题不嵌套,则此替换将删除字符串开头(隐式$_)和第一个嵌入标题结尾之间的所有内容。它通过使用*?量词而不是通常的*来实现这一点。如果没有问号,模式将匹配(并且替换将删除)字符串中最后一个标题的结尾。回想一下,尾随的s修饰符允许我们的标题跨行。

Assuming that the HTML input is well formed, and that headers do not nest, this substitution deletes everything between the beginning of the string (implicitly $_) and the end of the first embedded header. It does so by using the *? quantifier instead of the usual *. Without the question marks, the pattern would match through (and the substitution would delete through) the end of the last header in the string. Recall that the trailing s modifier allows our headers to span lines.

通常,*?匹配前面子表达式的最小实例数,以使整体匹配成功。同样,+?匹配至少一个实例,但不超过使整体匹配成功所需的实例数,而??匹配零个或一个实例,优先匹配零个。■

In general, *? matches the smallest number of instances of the preceding subexpression that will allow the overall match to succeed. Similarly, +? matches at least one instance, but no more than necessary to allow the overall match to succeed, and ?? matches either zero or one instances, with a preference for zero. ■

变量插值和捕获

Variable Interpolation and Capture

例 14.54

Example 14.54

扩展 RE 中的变量插值

Variable interpolation in extended REs

与双引号字符串一样,Perl 中的正则表达式支持变量插值。任何不在竖线、右括号或字符串结尾前面的美元符号都被视为引入 Perl 变量的名称,该变量的值在将模式传递给正则表达式求值器之前会以字符串形式展开。这使我们能够编写在运行时生成模式的代码:

Like double-quoted strings, regular expressions in Perl support variable interpolation. Any dollar sign that does not immediately precede a vertical bar, closing parenthesis, or end of string is assumed to introduce the name of a Perl variable, whose value as a string is expanded prior to passing the pattern to the regular expression evaluator. This allows us to write code that generates patterns at run time:

$前缀 = …

$prefix = …

$后缀 = …

$suffix = …

如果 ($foo =~ /^$prefix.*$suffix$/) …

if ($foo =~ /^$prefix.*$suffix$/) …

请注意本例中$扮演的两种不同角色。■

Note the two different roles played by $ in this example. ■

例 14.55

Example 14.55

扩展 Res 中的变量捕获

Variable capture in extended Res

信息流也可以反过来:我们可以从正则表达式中提取变量的值。我们在图 14.1sed脚本中看到了一个简单的例子:

The flow of information can go the other way as well: we can pull the values of variables out of regular expressions. We saw a simple example in the sed script of Figure 14.1:

s/^.*\(<[hH][123]>\)/\1/ ;# 删除打开标签前的文本

s/^.*\(<[hH][123]>\)/\1/ ;# delete text before opening tag

Perl 中的对应代码如下所示:

The equivalent in Perl would look something like this:

$line =~ s/^.*(<[hH][123]>)/\1/;

$line =~ s/^.*(<[hH][123]>)/\1/;

Perl RE 的每个带括号的片段都被认为捕获了它匹配的文本。捕获的字符串可以在替换的右侧引用为\1\2等。在表达式之外,它们仍然可用(直到执行下一个替换)为$1$2等:

Every parenthesized fragment of a Perl RE is said to capture the text that it matches. The captured strings may be referenced in the right-hand side of the substitution as \1, \2, and so on. Outside the expression they remain available (until the next substitution is executed) as $1, $2, and so on:

打印“打开标签:”,$1,“\n”;

print “Opening tag: “, $1, “\n”;

设计与实现

Design & Implementation

14.10 编译正则表达式

14.10 Compiling regular expressions

在将正则表达式用作搜索的基础之前,必须将其编译为确定性或非确定性(回溯)自动机。显然是常量的模式可以编译一次,无论是在程序加载时还是在第一次遇到时。但是,包含插值字符串的模式通常必须在遇到时重新编译,这可能会产生很大的运行时成本。知道插值变量永远不会改变的程序员可以通过在正则表达式后面附加一个o修饰符来抑制重新编译,在这种情况下,表达式将在第一次遇到时进行编译,之后再也不会编译。对于有时但并非总是必须重新编译的表达式,程序员可以使用 qr运算符强制重新编译模式,从而产生可以重复且高效使用的结果:

Before it can be used as the basis of a search, a regular expression must be compiled into a deterministic or nondeterministic (backtracking) automaton. Patterns that are clearly constant can be compiled once, either when the program is loaded or when they are first encountered. Patterns that contain interpolated strings, however, must in the general case be recompiled whenever they are encountered, at potentially significant run-time cost. A programmer who knows that interpolated variables will never change can inhibit recompilation by attaching a trailing o modifier to the regular expression, in which case the expression will be compiled the first time it is encountered, and never thereafter. For expressions that must sometimes but not always be recompiled, the programmer can use the qr operator to force recompilation of a pattern, yielding a result that can be used repeatedly and efficiently:

for (@patterns) { # 迭代模式

for (@patterns) { # iterate over patterns

 my $pat = qr($_); # 编译为自动机

 my $pat = qr($_); # compile to automaton

 for (@strings) { # 迭代字符串

 for (@strings) { # iterate over strings

  if (/$pat/) { # 无需重新编译

  if (/$pat/) { # no recompilation required

   print;#打印所有匹配的字符串

   print; # print all strings that match

   打印“\n”;

   print “\n”;

  }

  }

 }

 }

 打印“\n”;

 print “\n”;

}

}

例 14.56

Example 14.56

扩展 RE 中的反向引用

Backreferences in extended REs

我们甚至可以稍后在 RE 本身中使用捕获的字符串。这样的字符串称为反向引用

One can even use a captured string later in the RE itself. Such a string is called a backreference:

如果 (/.*?(<[hH]([123])>.*?<\/[hH]\2>)/) {

if (/.*?(<[hH]([123])>.*?<\/[hH]\2>)/) {

 打印“标题:$1\n”;

 print “header: $1\n”;

}

}

这里我们使用了\2来确保 HTML 标头的结束标记与开始标记相匹配。■

Here we have used \2 to insist that the closing tag of an HTML header match the opening tag. ■

例 14.57

Example 14.57

解析浮点文字

Dissecting a floating-point literal

当然可以捕获多个字符串:

One can, of course capture multiple strings:

如果 (/^([+-]?)((\d+)\.|(\d*)\.(\d+))(e([+-]?\d+))?$/) {

if (/^([+-]?)((\d+)\.|(\d*)\.(\d+))(e([+-]?\d+))?$/) {

 # 浮点数

 # floating-point number

 打印“符号:”,$1,“\n”;

 print “sign: “, $1, “\n”;

 打印“整数:”,$3,$4,“\n”;

 print “integer: “, $3, $4, “\n”;

 打印“分数:”,$5,“\n”;

 print “fraction: “, $5, “\n”;

 打印“尾数:”,$2,“\n”;

 print “mantissa: “, $2, “\n”;

 打印“指数:”,$7,“\n”;

 print “exponent: “, $7, “\n”;

}

}

与上例一样,编号对应于左括号的出现,从左到右阅读。输入-123.45e-6时,我们会看到

As in the previous example, the numbering corresponds to the occurrence of left parentheses, read from left to right. With input -123.45e-6 we see

符号: -

sign: -

整数:123

integer: 123

分数:45

fraction: 45

尾数:123.45

mantissa: 123.45

指数:-6

exponent: -6

请注意,由于交替,$3$4中肯定有一个会被设置。还请注意,虽然我们需要第六组括号来进行分组(它有一个?量词),但我们实际上并不需要它来进行捕获。■

Note that because of alternation, exactly one of $3 and $4 is guaranteed to be set. Note also that while we need the sixth set of parentheses for grouping (it has a ? quantifier), we don't really need it for capture. ■

例 14.58

Example 14.58

隐式捕获前缀、匹配和后缀

Implicit capture of prefix, match, and suffix

对于简单匹配,Perl 还提供了名为$'$&$'的伪变量。它们分别命名最近匹配之前、之中和之后的字符串部分:

For simple matches, Perl also provides pseudovariables named $', $&, and $'. These name the portions of the string before, in, and after the most recent match, respectively:

$行=<>;

$line = <>;

chop $line; # 删除尾随的换行符

chop $line; # delete trailing newline

$line =~ /是/;

$line =~ /is/;

打印“前缀($')匹配($&)后缀($')\n”;

print “prefix($') match($&) suffix($')\n”;

输入“ now is the time ”,此代码打印

With input “now is the time“, this code prints

前缀(现在)匹配(是)后缀(时间)

prefix(now) match(is) suffix(the time)

14-01-9780124104099检查你的理解

Check Your Understanding

32. 说出一种使用动态作用域的脚本语言。

32. Name a scripting language that uses dynamic scoping.

33. 总结Perl、PHP、Ruby和Python中用于确定未声明变量范围的策略。

33. Summarize the strategies used in Perl, PHP, Ruby, and Python to determine the scope of variables that are not declared.

34. 描述 Perl 中动态作用域变量的概念模型。

34. Describe the conceptual model for dynamically scoped variables in Perl.

35. 列出 POSIX 正则表达式中存在、但形式语言理论正则表达式中不存在的主要特征(第 2.1.1 节)。

35. List the principal features found in POSIX regular expressions, but not in the regular expressions of formal language theory (Section 2.1.1).

36. 列出 Perl RE 中发现的、但 POSIX 中没有的主要特性。

36. List the principal features found in Perl REs, but not in those of POSIX.

37.解释 Perl 类型正则表达式中搜索修饰符(最终分隔符后面的字符)的用途。

37. Explain the purpose of search modifiers (characters following the final delimiter) in Perl-type regular expressions.

38.描述 Perl 类型正则表达式中转义序列的三个主要类别。

38. Describe the three main categories of escape sequences in Perl-type regular expressions.

39.解释 贪婪匹配和最小匹配之间的区别。

39. Explain the difference between greedy and minimal matches.

40.描述 正则表达式中的捕获概念。

40. Describe the notion of capture in regular expressions.

14.4.3 数据类型

14.4.3 Data Types

例 14.59

Example 14.59

Ruby 和 Perl 中的强制转换

Coercion in Ruby and Perl

正如我们所见,脚本语言通常不需要(甚至不允许)声明变量的类型。大多数脚本语言会执行广泛的运行时检查,以确保值永远不会以不适当的方式使用。有些语言(例如 Scheme、Python 和 Ruby)对这种检查相对严格;想要从一种类型转换为另一种类型的程序员必须明确说明。如果我们在 Ruby 中输入以下内容,

As we have seen, scripting languages don't generally require (or even permit) the declaration of types for variables. Most perform extensive run-time checks to make sure that values are never used in inappropriate ways. Some languages (e.g., Scheme, Python, and Ruby) are relatively strict about this checking; the programmer who wants to convert from one type to another must say so explicitly. If we type the following in Ruby,

a =“4”

a = “4”

打印 a + 3,“\n”

print a + 3, “\n”

我们在运行时收到以下消息:“在'+'中:没有将 Fixnum 隐式转换为 String (TypeError)。” Perl 的宽容度要高得多。正如我们在示例 14.2中看到的那样,程序

we get the following message at run time: “In '+': no implicit conversion of Fixnum into String (TypeError).” Perl is much more forgiving. As we saw in Example 14.2, the program

$a =“4”;
打印 $a.3.“\n”;# '.' 是连接
打印 $a + 3 . “\n”;# '+' 表示加法

印刷品 43 和 7。■

prints 43 and 7. ■

例 14.60

Example 14.60

Perl 中的强制转换和上下文

Coercion and context in Perl

通常,Perl(以及 Rexx 和 Tcl)认为程序员应该检查他们关心的错误,如果没有这样的检查,程序应该做一些合理的事。例如,Perl 愿意接受以下内容(尽管如果使用 -w 编译开关运行,它会打印警告):

In general, Perl (and likewise Rexx and Tcl) takes the position that programmers should check for the errors they care about, and in the absence of such checks the program should do something reasonable. Perl is willing, for example, to accept the following (though it prints a warning if run with the -w compile-time switch):

$a[3] = “1”;#(数组@a 先前未定义)
打印$a[3] + $a[4],“\n”;

这里$a[4]未初始化,因此值为undef。在数字上下文中(作为+的操作数),字符串“1”的计算结果为 1,undef 的计算结果为 0。将它们相加得到 1,将其转换为字符串并打印出来。■

Here $a[4] is uninitialized and hence has value undef. In a numeric context (as an operand of +) the string “1” evaluates to 1, and undef evaluates to 0. Added together, these yield 1, which is converted to a string and printed. ■

例 14.61

Example 14.61

Ruby 中的显式转换

Explicit conversion in Ruby

Ruby 中的类似代码片段需要多加注意。在对a进行下标之前,我们必须确保它引用一个数组:

A comparable code fragment in Ruby requires a bit more care. Before we can subscript a we must make sure that it refers to an array:

a = []# 空数组赋值
a[3] = “1”

如果第一行不存在(并且a没有以任何其他方式初始化),第二行将生成“未定义局部变量”错误。在这些赋值之后,a[3]是一个字符串,但a的其他元素为nil。我们不能连接字符串和nil ,也不能将它们相加(这两个运算符在 Ruby 中都是使用运算符+指定的)。如果我们想要连接,并且a[4]可能为nil,我们必须说

If the first line were not present (and a had not been initialized in any other way), the second line would have generated an “undefined local variable” error. After these assignments, a[3] is a string, but other elements of a are nil. We cannot concatenate a string and nil, nor can we add them (both operators are specified in Ruby using the operator +). If we want concatenation, and a[4] may be nil, we must say

打印 a[3] + String(a[4]),“\n”

print a[3] + String(a[4]), “\n”

如果我们想要添加,我们必须说

If we want addition, we must say

打印整数(a[3])+整数(a[4]),“\n”

print Integer(a[3]) + Integer(a[4]), “\n”

正如这些示例所示,Perl(以及 Tcl)使用变量的值模型。Scheme、Python 和 Ruby 使用引用模型。PHP 和 JavaScript 与 Java 一样,对原始类型的变量使用值模型,对对象类型的变量使用引用模型。这种区别在 PHP 和 JavaScript 中不如在 Java 中那么重要,因为同一个变量可以在一个时间点保存原始值,而在另一个时间点保存对象引用。

As these examples suggest, Perl (and likewise Tcl) uses a value model of variables. Scheme, Python, and Ruby use a reference model. PHP and JavaScript, like Java, use a value model for variables of primitive type and a reference model for variables of object type. The distinction is less important in PHP and JavaScript than it is in Java, because the same variable can hold a primitive value at one point in time and an object reference at another.

数字类型

Numeric Types

正如我们在14.4.2 节中看到的,脚本语言通常提供一组非常丰富的字符串和模式操作机制。语法和插值约定各不相同,但底层功能非常一致,并且深受 Perl 的影响。对数值类型的底层支持在不同语言中表现出更多的差异,但编程模型同样非常一致:大致上,鼓励用户将数值视为“简单的数字”,而不必担心定点和浮点之间的区别,也不必担心可用精度的限制。

As we have seen in Section 14.4.2, scripting languages generally provide a very rich set of mechanisms for string and pattern manipulation. Syntax and interpolation conventions vary, but the underlying functionality is remarkably consistent, and heavily influenced by Perl. The underlying support for numeric types shows a bit more variation across languages, but the programming model is again remarkably consistent: users are, to first approximation, encouraged to think of numeric values as “simply numbers,” and not to worry about the distinction between fixed and floating point, or about the limits of available precision.

在内部,JavaScript 中的数字始终是双精度浮点数;在 Lua 中,它们默认也是双精度数。在 Tcl 中,它们是字符串,在需要算术时转换为整数或浮点数(并再次转换回来)。PHP 使用整数(保证至少 32 位宽),加上双精度浮点数。Perl 和 Ruby 为这些数字添加了任意精度(多字)整数,有时称为bignum s。Python 也有 bignums,并且支持复数。Scheme 具有上述所有功能,以及精确有理数,以 (分子,分母) 对的形式维护。在所有情况下,解释器在对具有不同表示形式的值进行算术运算时,或者在发生溢出时,都会根据需要进行“向上转换”。

Internally, numbers in JavaScript are always double-precision floating point; they are doubles by default in Lua as well. In Tcl they are strings, converted to integers or floating-point numbers (and back again) when arithmetic is needed. PHP uses integers (guaranteed to be at least 32 bits wide), plus double-precision floating point. To these Perl and Ruby add arbitrary precision (multiword) integers, sometimes known as bignums. Python has bignums too, plus support for complex numbers. Scheme has all of the above, plus precise rationals, maintained as (numerator, denominator) pairs. In all cases the interpreter “up-converts” as necessary when doing arithmetic on values with different representations, or when overflow would otherwise occur.

Perl 非常谨慎地隐藏了不同数字表示法之间的区别。大多数其他语言允许用户决定使用哪种表示法,尽管这很少是必要的。Ruby 可能对不同表示法的存在最为明确:类FixnumBignumFloat(双精度浮点)具有重叠但不完全相同的内置方法集。特别是,整数具有迭代器方法,而浮点数没有,浮点数具有舍入和错误检查方法,而整数没有。FixnumBignum都是Integer的后代。

Perl is scrupulous about hiding the distinctions among different numeric representations. Most other languages allow the user to determine which is being used, though this is seldom necessary. Ruby is perhaps the most explicit about the existence of different representations: classes Fixnum, Bignum, and Float (double-precision floating point) have overlapping but not identical sets of built-in methods. In particular, integers have iterator methods, which floating-point numbers do not, and floating-point numbers have rounding and error checking methods, which integers do not. Fixnum and Bignum are both descendants of Integer.

复合类型

Composite Types

选择 C、Fortran 和 Ada 等编译语言的类型构造函数主要是为了高效实现。特别是数组和记录,它们具有直接的时间和空间效率实现,我们在第8 章中学习过。然而,效率在脚本语言中并不那么重要。设计人员可以自由选择类型构造函数,这些构造函数更注重易于理解而不是纯粹的运行时性能。特别是,大多数脚本语言都非常重视映射,有时也称为字典、哈希关联数组。从这些名称中的第三个可能猜出,映射通常是用哈希表实现的。哈希的访问时间仍然为 0(1),但其常数明显高于编译数组或记录的典型值。

The type constructors of compiled languages like C, Fortran, and Ada were chosen largely for the sake of efficient implementation. Arrays and records, in particular, have straightforward time- and space-efficient implementations, which we studied in Chapter 8. Efficiency, however, is less important in scripting languages. Designers have felt free to choose type constructors oriented more toward ease of understanding than pure run-time performance. In particular, most scripting languages place a heavy emphasis on mappings, sometimes called dictionaries, hashes, or associative arrays. As might be guessed from the third of these names, a mapping is typically implemented with a hash table. Access time for a hash remains 0(1), but with a significantly higher constant than is typical for a compiled array or record.

Perl 是使用最广泛的脚本语言中历史最悠久的一种,它从awk继承了其主要的复合类型(数组和哈希) 。它还在变量名上使用前缀字符来指示类型:$foo是标量(数字、布尔值、字符串或指针 [Perl 称之为“引用”]);@foo是数组;%foo是哈希;&foo是子例程;而普通的foo是文件句柄或 I/O 格式,具体取决于上下文。

Perl, the oldest of the widely used scripting languages, inherits its principal composite types—the array and the hash—from awk. It also uses prefix characters on variable names as an indication of type: $foo is a scalar (a number, Boolean, string, or pointer [which Perl calls a “reference”]); @foo is an array; %foo is a hash; &foo is a subroutine; and plain foo is a filehandle or an I/O format, depending on context.

例 14.62

Example 14.62

Perl 数组

Perl arrays

Perl 中的普通数组使用方括号和从 0 开始的整数进行索引:

Ordinary arrays in Perl are indexed using square brackets and integers starting with 0:

@colors = (“红色”, “绿色”, “蓝色”);# 初始化语法
打印$colors[1];# 绿色的

请注意,我们在引用整个数组时使用@前缀,在引用其中一个(标量)元素时使用$前缀。数组是自扩展的:分配给越界元素只会使数组变大(以动态内存分配和复制为代价)。未初始化的元素默认具有undef值。■

Note that we use the @ prefix when referring to the array as a whole, and the $ prefix when referring to one of its (scalar) elements. Arrays are self-expanding: assignment to an out-of-bounds element simply makes the array larger (at the cost of dynamic memory allocation and copying). Uninitialized elements have the value undef by default. ■

例 14.63

Example 14.63

Perl 哈希

Perl hashes

哈希使用花括号和字符串名称进行索引:

Hashes are indexed using curly braces and character string names:

“%补充=(“红色”=>“青色”,

“%complements = (“red” => “cyan”,

 “绿色” => “洋红色”,“蓝色” => “黄色”);

 “green” => “magenta”, “blue” => “yellow”);

打印 $complements{“blue”}; # 黄色

print $complements{“blue”}; # yellow

这些也是自我扩展的。

These, too, are self-expanding.

记录和对象通常由哈希值构建。C 程序员会写fred.age = 19,而 Perl 程序员会写$fred{“age”} = 19。在面向对象代码中,$fred更可能是一个引用,在这种情况下,我们有$fred->{“age”} = 19。

Records and objects are typically built from hashes. Where the C programmer would write fred.age = 19, the Perl programmer writes $fred{“age”} = 19. In object-oriented code, $fred is more likely to be a reference, in which case we have $fred->{“age”} = 19.

例 14.64

Example 14.64

Python 和 Ruby 中的数组和哈希

Arrays and hashes in Python and Ruby

Python 和 Ruby 与 Perl 一样,提供常规数组和哈希。它们在两种情况下都使用方括号进行索引,并分别使用括号和大括号分隔符区分数组和哈希初始化器(聚合):

Python and Ruby, like Perl, provide both conventional arrays and hashes. They use square brackets for indexing in both cases, and distinguish between array and hash initializers (aggregates) using bracket and brace delimiters, respectively:

颜色 = [“红色”,“绿色”,“蓝色”]

colors = [“red”, “green”, “blue”]

补充 = {“红色” => “青色”,

complements = {“red” => “cyan”,

 “绿色” => “洋红色”,“蓝色” => “黄色”}

 “green” => “magenta”, “blue” => “yellow”}

打印颜色[2],补色[“蓝色”]

print colors[2], complements[“blue”]

设计与实现

Design & Implementation

14.11 Perl 中的类型团

14.11 Typeglobs in Perl

事实证明,Perl 中的全局名称可以具有多个独立的含义。例如,可以在同一个程序中使用$foo、@foo、%foo 和 &foo以及foo的两种不同含义。为了跟踪这些多重含义,Perl 在foo的符号表条目和foo可能具有的各种值之间插入了一个间接级别。中间结构称为typeglob 。它为foo的每个含义都有一个位置。它还有自己的名字:*foo。通过操作 typeglob,专业的 Perl 程序员实际上可以修改解释器用来在运行时查找名称的表。最简单的用法是创建别名:

It turns out that a global name in Perl can have multiple independent meanings. It is possible, for example, to use $foo, @foo, %foo, &foo and two different meanings of foo, all in the same program. To keep track of these multiple meanings, Perl interposes a level of indirection between the symbol table entry for foo and the various values foo may have. The intermediate structure is called a typeglob. It has one slot for each of foo's meanings. It also has a name of its own: *foo. By manipulating typeglobs, the expert Perl programmer can actually modify the table used by the interpreter to look up names at run time. The simplest use is to create an alias:

*a = *b;

*a = *b;

执行此语句后,ab就无法区分了;它们都指向同一个类型团,对其中一个类型团(任何含义)的更改将通过另一个类型团可见。Perl 还支持选择性别名,其中类型团的一个位置被设置为指向来自不同类型团的值:

After executing this statement, a and b are indistinguishable; they both refer to the same typeglob, and changes made to (any meaning of) one of them will be visible through the other. Perl also supports selective aliasing, in which one slot of a typeglob is made to point to a value from a different typeglob:

*a = \&b;

*a = \&b;

Perl 中的反斜杠运算符 ( \ ) 用于创建指针。执行此语句后,&a ( a作为函数的含义)将与&b相同,但a的所有其他含义将保持不变。除其他外,使用选择性别名来实现 Perl 中从库导入名称的机制。

The backslash operator (\) in Perl is used to create a pointer. After executing this statement, &a (the meaning of a as a function) will be the same as &b, but all other meanings of a will remain the same. Selective aliasing is used, among other things, to implement the mechanism that imports names from libraries in Perl.

(这是 Ruby 语法;Python 使用 : 代替 =>。)■

(This is Ruby syntax; Python uses : in place of =>.) ■

例 14.65

Example 14.65

Ruby 中的数组访问方法

Array access methods in Ruby

作为一种纯粹的面向对象语言,Ruby 将下标定义为调用 [] (get)和 []= (put)方法的语法糖:

As a purely object-oriented language, Ruby defines subscripting as syntactic sugar for invocations of the [] (get) and []= (put) methods:

c = 颜色[2]#与c = colors.[](2)相同
颜色[2] = c#与颜色相同。[]=(2, c)

例 14.66

Example 14.66

Python 中的元组

Tuples in Python

除了数组(称为列表)和哈希(称为字典)之外,Python 还提供了另外两种复合类型:元组和集合。元组本质上是一个不可变的列表(数组)。初始化语法使用圆括号而不是方括号:

In addition to arrays (which it calls lists) and hashes (which it calls dictionaries), Python provides two other composite types: tuples and sets. A tuple is essentially an immutable list (array). The initializer syntax uses parentheses rather than brackets:

深红色 = (0xdc, 0x14, 0x3c) # R、G、B 分量

crimson = (0xdc, 0x14, 0x3c)  # R,G,B components

元组的访问效率比数组更高:它们的不变性消除了大多数边界和调整大小检查的需要。它们还构成了多路分配的基础:

Tuples are more efficient to access than arrays: their immutability eliminates the need for most bounds and resizing checks. They also form the basis of multiway assignment:

a,b = b,a #交换

a, b = b, a    #swap

在这个例子中,可以省略括号:逗号比赋值运算符组合得更紧密。■

Parentheses can be omitted in this example: the comma groups more tightly than the assignment operator. ■

例 14.67

Example 14.67

Python 中的集合

Sets in Python

Python 集合类似于字典,不映射到任何感兴趣的内容,而只是用于指示元素是否存在。与字典不同,它们还支持并集、交集和差集运算:

Python sets are like dictionaries that don't map to anything of interest, but simply serve to indicate whether elements are present or absent. Unlike dictionaries, they also support union, intersection, and difference operations:

X = 设置(['a','b','c','d'])# 设置构造函数
Y = 设置(['c','d','e','f'])# 以数组作为参数
U = X | Y#([‘a’,‘b’,‘c’,‘d’,‘e’,‘f’])
我=X&Y# (['光盘'])
D=X-Y([‘a’,‘b’])
零=X^Y#([‘a’,‘b’,‘e’,‘f’])
I 中的“c”# 真的

例 14.68

Example 14.68

PHP、Tcl 和 JavaScript 中的混合类型

Conflated types in PHP, Tcl, and JavaScript

PHP 和 Tcl 具有更简单的复合类型:它们消除了数组和哈希之间的区别。数组只是程序员选择使用数字键的哈希。JavaScript 采用了类似的简化,统一了数组、哈希和对象。通常用于访问对象成员(JavaScript 称之为属性)的obj.attr表示法只是obj[“attr”]的语法糖。因此,对象是哈希,数组是具有整数属性名称的对象。■

PHP and Tcl have simpler composite types: they eliminate the distinction between arrays and hashes. An array is simply a hash for which the programmer chooses to use numeric keys. JavaScript employs a similar simplification, unifying arrays, hashes, and objects. The usual obj.attr notation to access a member of an object (what JavaScript calls a property) is simply syntactic sugar for obj[“attr”]. So objects are hashes, and arrays are objects with integer property names. ■

例 14.69

Example 14.69

Python 和其他语言中的多维数组

Multidimensional arrays in Python and other languages

在大多数脚本语言中,高维类型都很容易创建:可以定义哈希数组(引用)、数组哈希(引用)等等。或者,也可以通过使用复合对象作为哈希中的键来创建“扁平化”实现。Python 中的元组尤其适用:

Higher-dimensional types are straightforward to create in most scripting languages: one can define arrays of (references to) hashes, hashes of (references to) arrays, and so on. Alternatively, one can create a “flattened” implementation by using composite objects as keys in a hash. Tuples in Python work particularly well:

矩阵 = {}# 空字典(哈希)
矩阵[2, 3] = 4# 键是 (2, 3)

这种习惯用法提供了多维数组的外观和功能,但没有多维数组的效率。Python 的扩展库提供了更高效的同构数组,只是语法略显笨拙。数值和统计脚本语言(如 Maple、Mathematica、Matlab 和 R)对多维数组的支持更为广泛。■

This idiom provides the appearance and functionality of multidimensional arrays, though not their efficiency. There exist extension libraries for Python that provide more efficient homogeneous arrays, with only slightly more awkward syntax. Numeric and statistical scripting languages, such as Maple, Mathematica, Matlab, and R, have much more extensive support for multidimensional arrays. ■

语境

Context

7.2.2 节中,我们定义了类型兼容性的概念,它决定了在静态类型语言中,哪些类型可以在哪些上下文中使用。在这个定义中,术语“上下文”是指有关如何使用值的信息。例如,在 C 语言中,人们可能会说在声明中

In Section 7.2.2 we defined the notion of type compatibility, which determines, in a statically typed language, which types can be used in which contexts. In this definition the term “context” refers to information about how a value will be used. In C, for example, one might say that in the declaration

双d=3;

double d = 3;

右侧的3出现在需要浮点数的上下文中。C 编译器3强制转换为double而不是int

the 3 on the right-hand side occurs in a context that expects a floating-point number. The C compiler coerces the 3 to make it a double instead of an int.

7.2.3 节中,我们继续定义了类型推断的概念,它允许编译器根据表达式的组成部分的类型以及在某些情况下出现的上下文来确定表达式的类型。我们在 ML 及其后代中看到了一个极端的例子,它们使用一种复杂的推断形式来确定大多数对象的类型,而无需声明。

In Section 7.2.3 we went on to define the notion of type inference, which allows a compiler to determine the type of an expression based on the types of its constituent parts and, in some cases, the context in which it appears. We saw an extreme example in ML and its descendants, which use a sophisticated form of inference to determine types for most objects without the need for declarations.

在兼容性和推理这两种情况下,上下文信息仅在编译时使用。Perl 扩展了上下文的概念,以驱动运行时做出的决策。更具体地说,Perl 中的每个运算符在编译时确定其每个参数是否应将该参数解释为标量列表。相反,每个参数(本身可能是嵌套运算符)都能够在运行时判断它占用哪种上下文,从而可以表现出不同的行为。

In both of these cases—compatibility and inference—contextual information is used at compile time only. Perl extends the notion of context to drive decisions made at run time. More specifically, each operator in Perl determines, at compile time, and for each of its arguments, whether that argument should be interpreted as a scalar or a list. Conversely each argument (which may itself be a nested operator) is able to tell, at run time, which kind of context it occupies, and can consequently exhibit different behavior.

例 14.70

Example 14.70

Perl 中的标量和列表上下文

Scalar and list context in Perl

举一个简单的例子,赋值运算符 ( = ) 根据其左侧的类型为其右侧提供标量或列表上下文。此类型在编译时始终已知,并且通常对普通读者来说很明显,因为左侧是一个名称,其前缀字符要么是美元符号 ( $ )(表示标量上下文),要么是 at ( @ ) 或百分号 ( % )(表示列表上下文)。如果我们写

As a simple example, the assignment operator (=) provides a scalar or list context to its right-hand side based on the type of its left-hand side. This type is always known at compile time, and is usually obvious to the casual reader, because the left-hand side is a name and its prefix character is either a dollar sign ($), implying a scalar context, or an at (@) or percent (%) sign, implying a list context. If we write

$time = gmtime();

$time = gmtime();

Perl 的标准gmtime()库函数将以字符串形式返回时间,例如“Wed May 6 04:36:30 2015”。另一方面,如果我们写

Perl's standard gmtime() library function will return the time as a character string, along the lines of “Wed May 6 04:36:30 2015”. On the other hand, if we write

@time_arry = gmtime();

@time_arry = gmtime();

相同的函数将返回(30, 36, 4, 6, 4, 115, 3, 125, 0 ),一个九元素数组,表示秒、分钟、小时、月份中的日期、月份(一月 = 0)、年份(从 1900 年开始计算)、星期几(星期日 = 0)、一年中的日期,以及(作为 0/1 布尔值)指示是否为闰年。■

the same function will return (30, 36, 4, 6, 4, 115, 3, 125, 0),a nine-element array indicating seconds, minutes, hours, day of month, month of year (with January = 0), year (counting from 1900), day of week (with Sunday = 0), day of year, and (as a 0/1 Boolean value) an indication of whether it's a leap year. ■

例 14.71

Example 14.71

使用wantarray确定调用上下文

Using wantarray to determine calling context

那么gmtime如何知道该做什么?通过调用内置函数wantarray。如果当前函数是在列表上下文中调用的,则返回 true,如果是在标量上下文中调用的,则返回false。按照惯例,函数通常通过在列表上下文中调用时返回空数组来指示错误,在标量上下文中调用时返回未定义值(undef ):

So how does gmtime know what to do? By calling the built-in function wantarray. This returns true if the current function was called in a list context, and false if it was called in a scalar context. By convention, functions typically indicate an error by returning the empty array when called in a list context, and the undefined value (undef) when called in a scalar context:

如果(出现问题){

if (something went wrong) {

 返回 wantarray ?():undef;

 return wantarray ? () : undef;

}

}

14.4.4 面向对象

14.4.4 Object Orientation

虽然 Perl 5 不是面向对象的语言,但它具有允许以面向对象风格进行编程的功能。7 PHP和 JavaScript 具有更简洁、更传统的面向对象功能,但两者都允许程序员使用更传统的命令式风格。Python 和 Ruby 是明确且统一的面向对象语言。

Though not an object-oriented language, Perl 5 has features that allow one to program in an object-oriented style.7 PHP and JavaScript have cleaner, more conventional-looking object-oriented features, but both allow the programmer to use a more traditional imperative style as well. Python and Ruby are explicitly and uniformly object-oriented.

Perl 使用变量值模型;对象始终通过指针访问。在 PHP 和 JavaScript 中,变量可以保存原始类型的值或对复合类型对象的引用。然而,与 Perl 不同的是,这些语言没有提供方法来表示引用本身,而只提供它所引用的对象。Python 和 Ruby 使用统一的引用模型。

Perl uses a value model for variables; objects are always accessed via pointers. In PHP and JavaScript, a variable can hold either a value of a primitive type or a reference to an object of composite type. In contrast to Perl, however, these languages provide no way to speak of the reference itself, only the object to which it refers. Python and Ruby use a uniform reference model.

在 Python 和 Ruby 中,类本身就是对象,就像在 Smalltalk 中一样。在 PHP 中,它们只是类型,就像在 C++、Java 或 C# 中一样。在 Perl 中,类只是查看包(命名空间)的另一种方式。JavaScript 有对象但没有类,这一点很特别;它的继承基于一个称为原型的概念,该概念最初由 Self 编程语言引入。

Classes are themselves objects in Python and Ruby, much as they are in Smalltalk. They are merely types in PHP, much as they are in C++, Java, or C#. Classes in Perl are simply an alternative way of looking at packages (namespaces). JavaScript, remarkably, has objects but no classes; its inheritance is based on a concept known as prototypes, initially introduced by the Self programming language.

Perl 5

Perl 5

Perl 5 中的对象支持归结为两个主要方面:(1) 将引用与包关联的“祝福”机制,以及 (2) 方法调用的特殊语法,可自动将对象引用或包名称作为初始参数传递给函数。虽然原则上任何引用都可以得到祝福,但通常的惯例是使用哈希,以便可以命名字段,如示例 14.63所示。

Object support in Perl 5 boils down to two main things: (1) a “blessing” mechanism that associates a reference with a package, and (2) special syntax for method calls that automatically passes an object reference or package name as the initial argument to a function. While any reference can in principle be blessed, the usual convention is to use a hash, so that fields can be named as shown in Example 14.63.

例 14.72

Example 14.72

Perl 中的一个简单类

A simple class in Perl

举一个非常简单的例子,请考虑图 14.19中的 Perl 代码。这里我们定义了一个包Integer,它充当类的角色。它有三个函数,其中一个(new)旨在用作构造函数,两个(setget)旨在用作访问器。根据这个定义,我们可以写成

As a very simple example, consider the Perl code of Figure 14.19. Here we have defined a package, Integer, that plays the role of a class. It has three functions, one of which (new) is intendedto be used as a constructor, andtwo ofwhich (set and get) are intended to be used as accessors. Given this definition we can write

f14-19-9780124104099
图 14.19 Perl 中的面向对象编程。将引用(对象)添加到Integer包中,可使Integer的函数充当对象的方法。
$c1 = 整数->新(2);# 整数::new(“整数”, 2)
$c2 = 新整数(3);# 替代语法
$c3 = 新的整数;# 未指定初始值

Integer->newnew Integer都是调用Integer:: new 的语法糖,带有一个额外的第一个参数,该参数包含包(类)的名称作为字符串。在函数new的第一行中,我们将此字符串赋值给变量$class。shift运算符返回伪变量 [函数的参数] 的第一个元素,并移位其余参数(如果有),以便在再次使用shift时可以看到它们。)然后,我们创建一个对新哈希的引用,将其存储在局部变量$self中,并调用bless运算符将其与相应的类关联。第二次调用shift时,我们将检索整数的初始值(如果有)。(“或”表达式 [ | | ] 允许我们在没有显式参数的情况下使用 0。)我们使用通常的 Perl 语法将此初始值赋值给$selfval字段,以取消引用指针并对哈希进行下标。最后,我们返回对新创建对象的引用。■

Both Integer->new and new Integer are syntactic sugar for calls to Integer:: new with an additional first argument that contains the name of the package (class) as a character string. In the first line of function new we assign this string into the variable $class. (The shift operator returns the first element of pseudovariable [the function's arguments], and shifts the remaining arguments, if any, so they will be seen if shift is used again.) We then create a reference to a new hash, store it in local variable $self, and invoke the bless operator to associate it with the appropriate class. With a second call to shift we retrieve the initial value for our integer, if any. (The “or” expression [ | | ] allows us to use 0 instead if no explicit argument was present.) We assign this initial value into the val field of $self using the usual Perl syntax to dereference a pointer and subscript a hash. Finally we return a reference to the newly created object. ■

例 14.73

Example 14.73

在 Perl 中调用方法

Invoking methods in Perl

一旦引用被祝福,Perl 就允许它与方法调用语法一起使用:c1->get()get c1()Integer:: get($c1)的语法糖。请注意,此调用将引用作为附加的第一个参数传递,而不是包的名称。鉴于示例 14.72中的$c1、$c2$c3的声明,以下代码

Once a reference has been blessed, Perl allows it to be used with method invocation syntax: c1->get() and get c1() are syntactic sugar for Integer:: get($c1). Note that this call passes a reference as the additional first parameter, rather than the name of a package. Given the declarations of $c1, $c2, and $c3 from Example 14.72, the following code

打印 $c1->get, “ “,$c2->get, “ “,$c3->get, “ “, “\n”;

print $c1->get, “ “, $c2->get, “ “, $c3->get, “ “, “\n”;

$c1->设置(4);$c2->设置(5);$c3->设置(6);

$c1->set(4); $c2->set(5); $c3->set(6);

打印 $c1->get, “ “,$c2->get, “ “,$c3->get, “ “, “\n”;

print $c1->get, “ “, $c2->get, “ “, $c3->get, “ “, “\n”;

将打印

will print

2 3 0

2 3 0

4 5 6

4 5 6

与 Perl 中通常的情况一样,如果参数列表为空,则可以省略括号。■

As usual in Perl, if an argument list is empty, the parentheses can be omitted. ■

例 14.74

Example 14.74

Perl 中的继承

Inheritance in Perl

Perl 中的继承是通过@ISA数组实现的,该数组在包的全局级别初始化。扩展前面的示例,我们可以定义一个从Integer继承的Tally类:

Inheritance in Perl is obtained by means of the @ISA array, initialized at the global level of a package. Extending the previous example, we might define a Tally class that inherits from Integer:

{ 包装理货;

{ package Tally;

 @ISA = (“整数”);

 @ISA = (“Integer”);

 子公司 {

 sub inc {

  我的 $self = shift;

  my $self = shift;

  $自身->{val}++;

  $self->{val}++;

 }

 }

}

}

$t1 = 新的Tally(3);

$t1 = new Tally(3);

$t1->inc;

$t1->inc;

$t1->inc;

$t1->inc;

打印 $t1->get, “\n”; # 打印 5

print $t1->get, “\n”; # prints 5

t1inc方法按预期工作。但是,当 Perl 看到对Tally::newTally::get 的调用(这两个方法实际上都不在包中)时,它会使用@ISA数组来定位可以找到这些方法的其他包。我们可以在@ISA数组中列出任意数量的包;Perl 支持多重继承。new 可能通过Tally而不是Integer来调用,这解释了图 14.19中使用shift来获取类名的原因。如果我们明确使用了“Integer”,那么在创建Tally对象时就不会获得所需的行为。■

The inc method of t1 works as one might expect. However when Perl sees a call to Tally::new or Tally::get (neither of which is actually in the package), it uses the @ISA array to locate additional package(s) in which these methods may be found. We can list as many packages as we like in the @ISA array; Perl supports multiple inheritance. The possibility that new may be called through Tally rather than Integer explains the use of shift to obtain the class name in Figure 14.19. If we had used “Integer” explicitly we would not have obtained the desired behavior when creating a Tally object. ■

例 14.75

Example 14.75

通过use base继承

Inheritance via use base

Perl 中的包(以及类)通常在单独的模块(文件)中声明。在这种情况下,除了修改@ISA 之外,还必须导入与超类相对应的模块。标准基础模块为这种组合操作提供了方便的语法,并且是指定继承关系的首选方式:

Most often packages (and thus classes) in Perl are declared in separate modules (files). In this case, one must import the module corresponding to a superclass in addition to modifying @ISA. The standard base module provides convenient syntax for this combined operation, and is the preferred way to specify inheritance relationships:

{ 包装理货;

{ package Tally;

 使用基数(“整数”);

 use base (“Integer”);

 

 

PHP 和 JavaScript

PHP and JavaScript

虽然 Perl 的机制足以创建面向对象程序,但动态查找使它们比同等的命令式程序慢,而且可以说语法不够优雅。对象对 PHP 和 JavaScript 来说更为根本。

While Perl's mechanisms suffice to create object-oriented programs, dynamic lookup makes them slower than equivalent imperative programs, and it seems fair to characterize the syntax as less than elegant. Objects are more fundamental to PHP and JavaScript.

PHP 4 提供了多种面向对象功能,这些功能在 PHP 5 中进行了重大修改。新版本的语言提供了 (类类型) 变量、接口和混合继承、抽象方法和类、最终方法和类、静态和常量成员以及访问控制说明符 ( public、protectedprivate ) 的参考模型,让人联想到 Java、C# 和 C++。与本小节讨论的所有其他语言不同,PHP 中的类声明必须包含所有成员 (字段和方法) 的声明,并且给定类中的成员集不能随后更改 (尽管当然可以声明具有其他成员的派生类)。

PHP 4 provided a variety of object-oriented features, which were heavily revised in PHP 5. The newer version of the language provides a reference model of (class-typed) variables, interfaces and mix-in inheritance, abstract methods and classes, final methods and classes, static and constant members, and access control specifiers (public, protected, and private) reminiscent of those of Java, C#, and C++. In contrast to all other languages discussed in this subsection, class declarations in PHP must include declarations of all members (fields and methods), and the set of members in a given class cannot subsequently change (though one can of course declare derived classes with additional members).

JavaScript 采用了一种不寻常的方法,它通过继承和动态方法分派来提供对象,而不提供类。这种语言被称为基于对象的,而不是面向对象的 。在 JavaScript 中,函数是一等实体,实际上是对象。方法只是一个由对象的属性(成员)引用的函数。当我们调用om时,关键字this将在m引用的函数执行期间引用o。同样,当我们调用new f时,this将在f执行期间引用一个新创建的(最初为空的)对象。因此,JavaScript 中的构造函数是一个函数,其目的是将值分配给新创建对象的属性(字段和方法)。

JavaScript takes the unusual approach of providing objects—with inheritance and dynamic method dispatch—without providing classes. Such a language is said to be object-based, as opposed to object-oriented. Functions are first-class entities in JavaScript—objects, in fact. A method is simply a function that is referred to by a property (member) of an object. When we call o.m, the keyword this will refer to o during the execution of the function referred to by m. Likewise when we call new f, this will refer to a newly created (initially empty) object during the execution of f. A constructor in JavaScript is thus a function whose purpose is to assign values into properties (fields and methods) of a newly created object.

与每个构造函数f关联的是一个对象f.prototype。如果对象o由f构造,那么每当我们尝试使用 o 本身不提供的属性时,JavaScript 都会在 f.prototype 中查找实际上,of.prototype 继承它未覆盖的任何内容。原型属性通常用于保存方法它们也可以用于常量或其他语言所称的“类变量”。

Associated with every constructor f is an object f.prototype. If object o was constructed by f, then JavaScript will look in f.prototype whenever we attempt to use a property of o that o itself does not provide. In effect, o inherits from f.prototype anything that it does not override. Prototype properties are commonly used to hold methods. They can also be used for constants or for what other languages would call “class variables.”

例 14.76

Example 14.76

JavaScript 中的原型

Prototypes in JavaScript

图 14.20说明了原型的使用。它大致相当于图 14.19的 Perl 代码。函数Integer用作构造函数。对Integer.prototype属性的赋值用于为Integer 构造的对象建立方法。使用图中的代码,我们可以编写

Figure 14.20 illustrates the use of prototypes. It is roughly equivalent to the Perl code of Figure 14.19. Function Integer serves as a constructor. Assignments to properties of Integer.prototype serve to establish methods for objects constructed by Integer. Using the code in the figure, we can write

f14-20-9780124104099
图 14.20 JavaScript 中的面向对象编程。Integer函数用作构造函数,对其原型对象成员的赋值用于建立方法。这些方法将可用于由 Integer 创建的任何没有相应成员的对象。

c2 = 新的整数(3);

c2 = new Integer(3);

c3 = 新的整数;

c3 = new Integer;

文档.写入(c2.get()+“  ”+c3.get()+“<br>”);

document.write(c2.get() + “&nbsp;&nbsp;” + c3.get() + “<br>”);

设置(4);c3.设置(5);

c2.set(4); c3.set(5);

文档.写入(c2.get()+“  ”+c3.get()+“<br>”);

document.write(c2.get() + “&nbsp;&nbsp;” + c3.get() + “<br>”);

此代码将打印

This code will print

三 0

3 0

4 5

4 5

例 14.77

Example 14.77

在 JavaScript 中重写实例方法

Overriding instance methods in JavaScript

有趣的是,缺乏正式的类概念意味着我们可以逐个对象地覆盖方法和字段:

Interestingly, the lack of a formal notion of class means that we can override methods and fields on an object-by-object basis:

c2.set = 新函数(“n”,“this.val = n * n;”);

c2.set = new Function(“n”, “this.val = n * n;”);

 // 匿名函数构造函数

 // anonymous function constructor

c2.set(3); c3.set(4); // 这些调用不同的方法!

c2.set(3); c3.set(4); // these call different methods!

文档.写入(c2.get()+“  ”+c3.get()+“<br>”);

document.write(c2.get() + “&nbsp;&nbsp;” + c3.get() + “<br>”);

如果自上一个示例以来没有发生任何其他变化,则此代码将打印

If nothing else has changed since the previous example, this code will print

9 4

9 4

例 14.78

Example 14.78

JavaScript 中的继承

Inheritance in JavaScript

为了获得继承的效果,我们可以写

To obtain the effect of inheritance, we can write

函数Tally(n){

function Tally(n) {

 this.base(n); // 调用基构造函数

 this.base(n); // call to base constructor

}

}

函数Tally_inc(){

function Tally_inc() {

 这个.val++;

 this.val++;

}

}

Tally.prototype = new Integer; // 继承方法

Tally.prototype = new Integer; // inherit methods

Tally.prototype.base = Integer; // 使基本构造函数可用

Tally.prototype.base = Integer; // make base constructor available

Tally.prototype.inc = Tally_inc; // 新方法

Tally.prototype.inc = Tally_inc; // new method

t1 = 新的Tally(3);

t1 = new Tally(3);

t1.inc();t1.inc();

t1.inc(); t1.inc();

文档.写入(t1.get()+“<br>”);

document.write(t1.get() + “<br>”);

此代码将打印 5。■

This code will print a 5. ■

ECMAScript 6 预计将于 2015 年正式发布,它为该语言添加了正式的类概念以及许多其他功能。类以向后兼容的方式定义 - 本质上是带有原型的构造函数的语法糖。

ECMAScript 6, expected to become official in 2015, adds a formal notion of classes to the language, along with a host of other features. Classes are defined in a backward compatible way—essentially as syntactic sugar for constructors with prototypes.

Python 和 Ruby

Python and Ruby

正如我们指出的,Python 和 Ruby 都是明确的面向对象的。两者都采用统一的变量引用模型。与 Smalltalk 一样,两者都包含一个对象层次结构,其中类本身由对象表示。Python 中的根类称为object;在 Ruby 中,它是Object

As we have noted, both Python and Ruby are explicitly object-oriented. Both employ a uniform reference model for variables. Like Smalltalk, both incorporate an object hierarchy in which classes themselves are represented by objects. The root class in Python is called object; in Ruby it is Object.

例 14.79

Example 14.79

Python 和 Ruby 中的构造函数

Constructors in Python and Ruby

在 Python 和 Ruby 中,每个类都有一个可区分的构造函数,该构造函数不能重载。在 Python 中是 _ _ init _ _;在 Ruby 中是initialise。在 Python 中创建一个新对象,可以这样说my_object = My_class ( args );在 Ruby 中说my_object = My_class.new ( args )。在每种情况下,args都会传递给构造函数。要使用不同数量或类型的参数实现重载的效果,必须安排单个构造函数明确检查其参数。我们在 Perl(图 14.19new例程中)和 JavaScript(图 14.20Integer函数中)中使用了类似的习惯用法。■

In both Python and Ruby, each class has a single distinguished constructor, which cannot be overloaded. In Python it is _ _init_ _; in Ruby it is initialize. To create a new object in Python one says my_object = My_class(args); in Ruby one says my_object = My_class.new(args). In each case the args are passed to the constructor. To achieve the effect of overloading, with different numbers or types of arguments, one must arrange for the single constructor to inspect its arguments explicitly. We employed a similar idiom in Perl (in the new routine of Figure 14.19) and JavaScript (in the Integer function of Figure 14.20). ■

就类的内容(成员)而言,Python 和 Ruby 都比 PHP 或更传统的面向对象语言更灵活。只需将新字段分配给它们即可将其添加到 Python 对象中:my_object.new_field = value。但是,方法集在首次定义类时就已经固定了。在 Ruby 中,只有方法在类外部可见(必须使用“put”和“get”方法来访问字段),并且所有方法都必须明确声明。但是,可以修改现有的类声明,添加或覆盖方法。甚至可以逐个对象地执行此操作。因此,同一类的两个对象可能不会显示相同的行为。

Both Python and Ruby are more flexible than PHP or more traditional object-oriented languages regarding the contents (members) of a class. New fields can be added to a Python object simply by assigning to them: my_object.new_field = value. The set of methods, however, is fixed when the class is first defined. In Ruby only methods are visible outside a class (“put” and “get” methods must be used to access fields), and all methods must be explicitly declared. It is possible, however, to modify an existing class declaration, adding or overriding methods. One can even do this on an object-by-object basis. As a result, two objects of the same class may not display the same behavior.

例 14.80

Example 14.80

在 Python 和 Ruby 中命名类成员

Naming class members in Python and Ruby

Python 和 Ruby 在其他许多方面都不同。在 Python 中,方法的初始参数是显式的;按照惯例,它通常命名为self。在 Ruby 中,self是一个关键字,它所代表的参数是不可见的。在 Ruby 中,任何以单个@符号开头的变量都是当前对象的字段。在 Python 方法中,使用对象成员必须显式命名对象。例如,必须编写self.print();仅print()是不够的。■

Python and Ruby differ in many other ways. The initial parameter to methods is explicit in Python; by convention it is usually named self. In Ruby self is a keyword, and the parameter it represents is invisible. Any variable beginning with a single @ sign in Ruby is a field of the current object. Within a Python method, uses of object members must name the object explicitly. One must, for example, write self.print(); just print() will not suffice. ■

Ruby 方法可以是publicprotectedprivate。8 Python 中的访问控制纯粹是惯例问题;方法和字段都是普遍可访问的。最后,Python 具有多重继承。Ruby 具有混合继承:一个类不能从多个祖先获取数据。然而,与大多数其他语言不同,Ruby 允许接口(混合)不仅定义方法的签名,还定义其实现(代码)。

Ruby methods may be public, protected, or private.8 Access control in Python is purely a matter of convention; both methods and fields are universally accessible. Finally, Python has multiple inheritance. Ruby has mix-in inheritance: a class cannot obtain data from more than one ancestor. Unlike most other languages, however, Ruby allows an interface (mix-in) to define not only the signatures of methods but also their implementation (code).

14-01-9780124104099检查你的理解

Check Your Understanding

41. 对比 Perl 和 Ruby 在错误检查和报告方面的理念。

41. Contrast the philosophies of Perl and Ruby with regard to error checking and reporting.

42. 将流行脚本语言的数字类型与 C 或 Fortran 等编译语言的数字类型进行比较。

42. Compare the numeric types of popular scripting languages to those of compiled languages like C or Fortran.

43. 什么是大数?哪些语言支持它们?

43. What are bignums? Which languages support them?

44. 什么是关联数组?它们有时还有哪些其他名称?

44. What are associative arrays? By what other names are they sometimes known?

45. 为什么大多数脚本语言不提供对记录的直接支持?

45. Why don't most scripting languages provide direct support for records?

46. Perl 中的 typeglob是什么?它有什么用途?

46. What is a typeglob in Perl? What purpose does it serve?

47. 描述Python 的元组集合类型。

47. Describe the tuple and set types of Python.

48. 解释 PHP 和 Tcl 中数组和哈希的统一。

48. Explain the unification of arrays and hashes in PHP and Tcl.

49. 解释 JavaScript 中数组和对象的统一。

49. Explain the unification of arrays and objects in JavaScript.

50. 解释如何使用元组和哈希来模拟 Python 中的多维数组。

50. Explain how tuples and hashes can be used to emulate multidimensional arrays in Python.

51.解释一下 Perl 中的 上下文概念。它与类型兼容性和类型推断有何关系?该语言的运算符定义的两个主要上下文是什么?

51. Explain the concept of context in Perl. How is it related to type compatibility and type inference? What are the two principal contexts defined by the language's operators?

52. 比较 Perl 5、PHP 5、JavaScript、Python 和 Ruby 采用的面向对象方法。

52. Compare the approaches to object orientation taken by Perl 5, PHP 5, JavaScript, Python, and Ruby.

53.  Perl 中引用的祝福是什么意思?

53. What is meant by the blessing of a reference in Perl?

54. JavaScript 中的 原型是什么?它们有什么用途?

54. What are prototypes in JavaScript? What purpose do they serve?

设计与实现

Design & Implementation

14.12 可执行类声明

14.12 Executable class declarations

Python 和 Ruby 都持有一个有趣的观点,即类声明是可执行代码。声明的详细说明将执行其中的代码。除此之外,我们还可以使用这种机制来实现条件编译的效果:

Both Python and Ruby take the interesting position that class declarations are executable code. Elaboration of a declaration executes the code inside. Among other things, we can use this mechanism to achieve the effect of conditional compilation:

class My_class # Ruby 代码

class My_class # Ruby code

 定义初始化(a,b)

 def initialize(a, b)

  @a = a; @b = b;

  @a = a; @b = b;

 结尾

 end

 如果expensive_function()

 if expensive_function()

  定义获取()

  def get()

   返回@a

   return @a

  结尾

  end

 别的

 else

  定义获取()

  def get()

   返回@b

   return @b

  结尾

  end

 结尾

 end

结尾

end

我们不需要在每次调用时计算get 内部的昂贵函数,而是提前计算一次,并定义适当的get 专门版本。

Instead of computing the expensive function inside get, on every invocation, we compute it once, ahead of time, and define an appropriate specialized version of get.

14.5 总结和结束语

14.5 Summary and Concluding Remarks

脚本语言主要用于控制和协调其他软件组件。尽管它们的起源可以追溯到 20 世纪 60 年代的解释型语言,但多年来它们在很大程度上被学术计算机科学所忽视。然而,随着对程序员生产力的日益重视以及万维网的爆炸式增长,脚本语言在业界和学术界都越来越受到关注和欢迎。商业开发人员和开源社区已经取得了许多重大进展。脚本语言很可能会在 21 世纪主导编程,而传统的编译型语言则越来越多地被视为专用工具。

Scripting languages serve primarily to control and coordinate other software components. Though their roots go back to interpreted languages of the 1960s, for many years they were largely ignored by academic computer science. With an increasing emphasis on programmer productivity, however, and with the explosion of the World Wide Web, scripting languages have seen enormous growth in interest and popularity, both in industry and in academia. Many significant advances have been made by commercial developers and by the open-source community. Scripting languages may well come to dominate programming in the 21st century, with traditional compiled languages more and more seen as special-purpose tools.

与传统同类相比,脚本语言更注重灵活性和表达的丰富性,而不是纯粹的运行时性能。其共同特点包括批处理和交互式使用、表达的经济性、缺乏声明、简单的范围规则、灵活的动态类型、易于访问其他程序、复杂的模式匹配和字符串操作以及高级数据类型。

In comparison to their traditional cousins, scripting languages emphasize flexibility and richness of expression over sheer run-time performance. Common characteristics include both batch and interactive use, economy of expression, lack of declarations, simple scoping rules, flexible dynamic typing, easy access to other programs, sophisticated pattern matching and string manipulation, and high-level data types.

设计与实现

Design & Implementation

14.13 越糟越好

14.13 Worse Is Better

任何关于脚本和“系统”语言的相对优缺点的讨论最终都不可避免地要解决表现力和灵活性与编译时安全性和性能之间的权衡。它还可能偏离“快速而粗糙”的应用程序与“精致”的应用程序的问题。在 Richard Gabriel 广为流传的文章 ( www.dreamsongs.com/WorseIsBetter.html ) 中可以找到对这场争论的有趣看法。1989 年在 Lucid Corp. 工作期间,Gabriel 发现自己在问为什么 Unix 和 C 如此成功吸引用户,而 Common Lisp(Lucid 的主要关注点)却没有。他的解释将 Common Lisp 所体现的“正确的事情”与 C 和 Unix 所体现的“更差即是更好”的哲学进行了对比。“正确的事情”强调完整、正确、一致和优雅的设计。 “更糟糕即更好”强调软件的快速开发,这种软件可以满足用户大多数时间的大部分需求,并且可以根据现场经验逐步调整和改进。许多脚本,尤其是 Perl,都符合“更糟糕即更好”的理念(Ruby 和 Scheme 的爱好者可能会不同意)。加布里埃尔则表示,他还没有下定决心;他的文章对这两种观点都进行了论证。

Any discussion of the relative merits of scripting and “systems” languages invariably ends up addressing the tradeoffs between expressiveness and flexibility on the one hand and compile-time safety and performance on the other. It may also digress into questions of “quick-and-dirty” versus “polished” applications. An interesting take on this debate can be found in the widely circulated essays of Richard Gabriel (www.dreamsongs.com/WorseIsBetter.html). While working for Lucid Corp. in 1989, Gabriel found himself asking why Unix and C had been so successful at attracting users, while Common Lisp (Lucid's principal focus) had not. His explanation contrasts “The Right Thing,” as exemplified by Common Lisp, with a “Worse Is Better” philosophy, as exemplified by C and Unix. “The Right Thing” emphasizes complete, correct, consistent, and elegant design. “Worse Is Better” emphasizes the rapid development of software that does most of what users need most of the time, and can be tuned and improved incrementally, based on field experience. Much of scripting, and Perl in particular, fits the “Worse Is Better” philosophy (Ruby and Scheme enthusiasts might beg to disagree). Gabriel, for his part, says he still hasn't made up his mind; his essays argue both points of view.

本章首先回顾了脚本的历史发展,从20 世纪 70 年代中期的命令解释器或shell程序开始,到随后出现的文本处理和报告生成工具。我们特别研究了“Bourne-again”shell、 bash以及 Unix 工具sedawk。我们还提到了数学和统计学等特殊用途领域,在这些领域中,脚本语言被广泛用于数据分析、可视化、建模和模拟。然后,我们转向了当今脚本的三个主导领域:“粘合”(协调)应用程序、配置和扩展以及万维网脚本。

We began our chapter by tracing the historical development of scripting, starting with the command interpreter, or shell programs of the mid-1970s, and the text processing and report generation tools that followed soon thereafter. We looked in particular at the “Bourne-again” shell, bash, and the Unix tools sed and awk. We also mentioned such special-purpose domains as mathematics and statistics, where scripting languages are widely used for data analysis, visualization, modeling, and simulation. We then turned to the three domains that dominate scripting today: “glue” (coordination) applications, configuration and extension, and scripting of the World Wide Web.

多年来,Perl 一直是最受欢迎的通用“胶水”语言,但 Python 和 Ruby 显然已经超越了它。多种脚本语言(包括 Python、Scheme 和 Lua)被广泛用于扩展复杂应用程序的功能。此外,许多商业软件包都有自己的专有扩展语言。

For many years, Perl was the most popular of the general-purpose “glue” languages, but Python and Ruby have clearly overtaken it at this point. Several scripting languages, including Python, Scheme, and Lua, are widely used to extend the functionality of complex applications. In addition, many commercial packages have their own proprietary extension languages.

Web 脚本有多种形式。在 HTTP 连接的服务器端,通用网关接口 (CGI) 标准允许使用 URI 来命名将用于生成动态内容的程序。或者,可以使用通常以 PHP 编写的网页嵌入脚本以用户不可见的方式创建动态内容。为了减少服务器负载并提高交互响应能力,脚本也可以在客户端浏览器中执行。JavaScript 是此领域的主要符号;它使用 HTML 文档对象模型 (DOM) 来操作网页元素。对于更苛刻的任务,可以指示许多浏览器运行 Java 小程序,该小程序将完全负责部分“屏幕空间”,但这种策略会带来安全问题,人们越来越认为这种策略是不可接受的。随着 HTML5 的出现,大多数动态内容(尤其是多媒体)都可以由浏览器直接处理。同时,XML 已成为结构化、与演示无关的信息的标准格式,可通过 XSL 进行加载时转换。

Web scripting comes in many forms. On the server side of an HTTP connection, the Common Gateway Interface (CGI) standard allows a URI to name a program that will be used to generate dynamic content. Alternatively, web-page-embedded scripts, often written in PHP, can be used to create dynamic content in a way that is invisible to users. To reduce the load on servers, and to improve interactive responsiveness, scripts can also be executed within the client browser. JavaScript is the dominant notation in this domain; it uses the HTML Document Object Model (DOM) to manipulate web-page elements. For more demanding tasks, many browsers can be directed to run a Java applet, which takes full responsibility for some portion of the “screen real estate,” but this strategy comes with security concerns that are increasingly viewed as unacceptable. With the emergence of HTML5, most dynamic content—multimedia in particular—can be handled directly by the browser. At the same time, XML has emerged as the standard format for structured, presentation-independent information, with load-time transformation via XSL.

由于脚本语言发展迅速,它们能够利用前面章节中介绍的许多最强大、最优雅的机制,包括一等函数和高阶函数、无限范围、迭代器、垃圾收集、列表理解和面向对象,更不用说扩展正则表达式和字典、集合和元组等高级数据类型。鉴于目前的趋势,脚本语言可能会变得越来越普遍,并继续成为语言创新的主要焦点。

Because of their rapid evolution, scripting languages have been able to take advantage of many of the most powerful and elegant mechanisms described in previous chapters, including first-class and higher-order functions, unlimited extent, iterators, garbage collection, list comprehensions, and object orientation—not to mention extended regular expressions and such high-level data types as dictionaries, sets, and tuples. Given current trends, scripting languages are likely to become increasingly ubiquitous, and to remain a principal focus of language innovation.

14.6 练习

14.6 Exercises

14.1 文件名“通配符”是否提供了标准正则表达式的表达能力?解释一下。

14.1 Does filename “globbing” provide the expressive power of standard regular expressions? Explain.

14.2 编写 shell 脚本

14.2 Write shell scripts to

(a) 将当前目录中所有文件名称中的空格替换为下划线。

(a) Replace blanks with underscores in the names of all files in the current directory.

(b) 通过在文件名称前面添加修改日期的文本表示来重命名当前目录下的每个文件。

(b) Rename every file in the current directory by prepending to its name a textual representation of its modification date.

(c) 在当前目录下的文件层次结构中查找所有eps文件,并创建任何缺失或过时的相应pdf文件。

(c) Find all eps files in the file hierarchy below the current directory, and create any corresponding pdf files that are missing or out of date.

(d) 打印当前目录下文件层次结构中给定谓词求值为真值的所有文件的名称。您的(引用的)谓词应在命令行中使用 Unix test命令的语法指定,其中一个或多个@符号代表候选文件的名称。

(d) Print the names of all files in the file hierarchy below the current directory for which a given predicate evaluates to true. Your (quoted) predicate should be specified on the command line using the syntax of the Unix test command, with one or more at signs (@) standing in for the name of the candidate file.

14.3 示例 14.16中,我们使用“$@”来引用传递给ll 的参数。如果我们删除引号会发生什么?(提示:对名称包含空格的文件尝试此操作!)阅读bash手册页并了解$@$*之间的区别。创建使用$*“$*”而不是“$@”的ll版本。解释发生了什么。

14.3 In Example 14.16 we used “$@” to refer to the parameters passed to ll. What would happen if we removed the quote marks? (Hint: Try this for files whose names contain spaces!) Read the man page for bash and learn the difference between $@ and $*. Create versions of ll that use $* or “$*” instead of “$@”. Explain what's going on.

14.4 

14.4 

(a)扩展 图 14.5、14.614.7的代码,尝试更温和地终止进程。您需要阅读标准kill命令的手册页。首先使用TERM信号。如果不起作用,请询问用户是否应该求助于KILL

(a) Extend the code in Figure 14.5, 14.6, or 14.7 to try to kill processes more gently. You'll want to read the man page for the standard kill command. Use a TERM signal first. If that doesn't work, ask the user if you should resort to KILL.

(b) 将您的解决方案扩展到部分 (a),以便脚本接受指定要使用的信号的可选参数。TERM和KILL替代方案包括HUP、INT、QUITABRT。

(b) Extend your solution to part (a) so that the script accepts an optional argument specifying the signal to be used. Alternatives to TERM and KILL include HUP, INT, QUIT, and ABRT.

14.5 编写一个 Perl、Python 或 Ruby 脚本来创建一个简单的索引:一个按顺序排列的出现在输入文档中的重要单词列表,每个单词都有一个子列表,指示该单词出现的行,以及最多六个上下文单词。从列表中排除所有常用冠词、连词、介词和代词。

14.5 Write a Perl, Python, or Ruby script that creates a simple concordance: a sorted list of significant words appearing in an input document, with a sublist for each that indicates the lines on which the word occurs, with up to six words of surrounding context. Exclude from your list all common articles, conjunctions, prepositions, and pronouns.

14.6 编写 Emacs Lisp 脚本

14.6 Write Emacs Lisp scripts to

(a) 将今天的日期插入到当前缓冲区的插入点(当前光标位置)。

(a) Insert today's date into the current buffer at the insertion point (current cursor location).

(b) 在插入点周围的单词上加上引号(“ “)。

(b) Place quote marks (“ “) around the word surrounding the insertion point.

(c) 修复当前缓冲区中的句末空格。使用以下启发式方法:如果句号、问号或感叹号后面跟着一个空格(中间可能有右引号、圆括号、方括号或花括号),则添加一个额外的空格,除非句号、问号或感叹号前面的字符是大写字母(在这种情况下我们假设它是缩写)。

(c) Fix end-of-sentence spaces in the current buffer. Use the following heuristic: if a period, question mark, or exclamation point is followed by a single space (possibly with closing quote marks, parentheses, brackets, or braces in-between), then add an extra space, unless the character preceding the period, question mark, or exclamation point is a capital letter (in which case we assume it is an abbreviation).

(d) 通过您最喜欢的拼写检查器运行当前缓冲区的内容,并创建一个包含拼写错误单词列表的新缓冲区。

(d) Run the contents of the current buffer through your favorite spell checker, and create a new buffer containing a list of misspelled words.

(e) 从(d)中创建的缓冲区中删除一个拼写错误的单词,并将光标(插入点)放在当前缓冲区中该拼写错误的单词第一次出现的上方。

(e) Delete one misspelled word from the buffer created in (d), and place the cursor (insertion point) on top of the first occurrence of that misspelled word in the current buffer.

14.7 解释在什么情况下将 Web 上的交互任务实现为 CGI 脚本、嵌入式服务器端脚本或客户端脚本是有意义的。对于每种实现选择,给出三个明显是首选方法的任务示例。

14.7 Explain the circumstances under which it makes sense to realize an interactive task on the Web as a CGI script, an embedded server-side script, or a client-side script. For each of these implementation choices, give three examples of tasks for which it is clearly the preferred approach.

14.8 

14.8 

(a) 编写一个嵌入 PHP 的网页,打印帕斯卡三角形的前 10 行(如果您不知道这是什么,请参阅示例 C-17.10)。渲染后,您的输出应如图14.21所示。

(a) Write a web page with embedded PHP to print the first 10 rows of Pascal's triangle (see Example C-17.10 if you don't know what this is). When rendered, your output should look like Figure 14.21.

f14-21-9780124104099
图 14.21 在网页中呈现的帕斯卡三角形(练习 14.8)。

(b) 修改您的页面以创建一个自发布表单,该表单接受输入字段中所需的行数。

(b) Modify your page to create a self-posting form that accepts the number of desired rows in an input field.

(c) 用 JavaScript 重写您的页面。

(c) Rewrite your page in JavaScript.

14.9 创建一个填写式 Web 表单,使用 Luhn 公式的 JavaScript 实现(练习 4.10)来检查信用卡号中的拼写错误。(但不要使用真实的信用卡号;家庭作业练习往往不太安全!)

14.9 Create a fill-in web form that uses a JavaScript implementation of the Luhn formula (Exercise 4.10) to check for typos in credit card numbers. (But don't use real credit card numbers; homework exercises don't tend to be very secure!)

14.10 

14.10 

(a)修改 图 14.15的代码(示例 14.35),使其用输出替换表单,就像图 14.1114.14的 CGI 和 PHP 版本所做的那样。

(a) Modify the code of Figure 14.15 (Example 14.35) so that it replaces the form with its output, as the CGI and PHP versions of Figures 14.11 and 14.14 do.

(b)修改 图 14.1114.14示例 14.3014.34 )的 CGI 和 PHP 脚本,使它们看起来将其输出附加到表单的底部,就像图 14.15的 JavaScript 版本一样。

(b) Modify the CGI and PHP scripts of Figures 14.11 and 14.14 (Examples 14.30 and 14.34) so they appear to append their output to the bottom of the form, as the JavaScript version of Figure 14.15 does.

14.11 在 Perl 中运行以下程序:sub foo { my $lex = $_[0]; sub bar { print “$lex\n”; } bar(); } foo(2); foo(3);您可能会对输出感到惊讶。Perl 5 允许命名子例程嵌套,但不能正确地为它们创建闭包。重写上述代码以创建对匿名本地子例程的引用,并验证它是否正确创建了闭包。将 use diagnostics ;行添加到原始版本的开头并再次运行它。根据这将给您的解释,推测嵌套命名子例程在 Perl 5 中是如何实现的。

 

  

  

   

  

  

 

 

14.11 Run the following program in Perl:

 sub foo {

  my $lex = $_[0];

  sub bar {

   print “$lex\n”;

  }

  bar();

 }

 foo(2); foo(3);

You may be surprised by the output. Perl 5 allows named subroutines to nest, but does not create closures for them properly. Rewrite the code above to create a reference to an anonymous local subroutine and verify that it does create closures correctly. Add the line use diagnostics; to the beginning of the original version and run it again. Based on the explanation this will give you, speculate as to how nested named subroutines are implemented in Perl 5.

14.12 编写一个程序,映射当前目录下文件层次结构中存储的网页。输出本身应为一个网页,包含所有目录和.html文件的名称,并以与文件层次结构中的级别相对应的缩进级别打印。每个.html文件名应为其文件的实际链接。使用最适合该任务的任何语言。

14.12 Write a program that will map the web pages stored in the file hierarchy below the current directory. Your output should itself be a web page, containing the names of all directories and .html files, printed at levels of indentation corresponding to their level in the file hierarchy. Each .html file name should be a live link to its file. Use whatever language(s) seem most appropriate to the task.

14.13 14.4.1 节中,我们声称 Ruby 中的嵌套块是它们出现的命名范围的一部分。通过运行以下 Ruby 脚本并解释其输出来验证此声明:def foo(x) y = 2 bar = proc { print x, “\n” y = 3 } bar.call() print y, “\n” end foo(3)现在注释掉第二行(y = 2)并再次运行脚本。解释发生了什么。更仔细、更准确地重述我们关于范围的声明。

 

  

  

   

   

  

  

  

 

 

14.13 In Section 14.4.1 we claimed that nested blocks in Ruby were part of the named scope in which they appear. Verify this claim by running the following Ruby script and explaining its output:

 def foo(x)

  y = 2

  bar = proc {

   print x, “\n”

   y = 3

  }

  bar.call()

  print y, “\n”

 end

 foo(3)

Now comment out the second line (y = 2) and run the script again. Explain what happens. Restate our claim about scoping more carefully and precisely.

14.14 编写一个 Perl 脚本,将英制测量单位(in、ft、yd、mi)转换为公制单位(cm、m、km)。您可能想要了解正则表达式中的e修饰符,它允许s///e表达式的右侧包含可执行代码。

14.14 Write a Perl script to translate English measurements (in, ft, yd, mi) into metric equivalents (cm, m, km). You may want to learn about the e modifier on regular expressions, which allows the right-hand side of an s///e expression to contain executable code.

14.15 编写一个 Perl 脚本,针对每个输入行,找出在该行中至少出现两次且不重叠的最长子字符串。(警告:这比听起来要难。请记住,默认情况下,Perl 会搜索最左边的最长匹配。)

14.15 Write a Perl script to find, for each input line, the longest substring that appears at least twice within the line, without overlapping. (Warning: This is harder than it sounds. Remember that by default Perl searches for a left-most longest match.)

14.16  Perl 提供了一种替代形式的括号(?:…),它支持在正则表达式中分组而不执行捕获。使用此语法,示例 14.57可以编写如下:if (/^([+-]?)((\d+)\.|(\d*)\.(\d+))(?:e([+-]?\d+))?$/) { # 浮点数print “sign: “, $1, “\n”; print “integer: “, $3, $4, “\n”; print “fraction: “, $5, “\n”; print “mantissa: “, $2, “\n”; print “exponent: “, $6, “\n”; # not $7 }这种额外的符号有什么用途?为什么这里的代码比示例 14.57的代码更可取

 

  

  

  

  

  

  

 

14.16 Perl provides an alternative (?:…) form of parentheses that supports grouping in regular expressions without performing capture. Using this syntax, Example 14.57 could have been written as follows:

 if (/^([+-]?)((\d+)\.|(\d*)\.(\d+))(?:e([+-]?\d+))?$/) {

  # floating-point number

  print “sign: “, $1, “\n”;

  print “integer: “, $3, $4, “\n”;

  print “fraction: “, $5, “\n”;

  print “mantissa: “, $2, “\n”;

  print “exponent: “, $6, “\n”; # not $7

 }

What purpose does this extra notation serve? Why might the code here be preferable to that of Example 14.57?

14.17 再考虑一下图 14.1中的sed代码。我们很容易将第一个复合语句写成如下形式(注意三个替换命令的区别):/<[hH][123]>.*<\/[hH][123]>/ { ;# 匹配整个标题h ;# 保存模式空间的副本s/^.*\(<[hH][123]>\)/\1/ ;# 删除开始标记之前的文本s/\(<\/[hH][123]>\).*$/\1/ ;# 删除结束标记之后的文本p ;# 打印剩余内容g ;# 检索保存的模式空间s/^.*<\/[hH][123]>// ;# 删除结束标记之前的内容b top解释为什么这样做不起作用。(提示:记住贪婪匹配最小匹配之间的区别 [示例 14.53 ]。Sed缺少后者。)

 

  

  

  

  

  

  

  

14.17 Consider again the sed code of Figure 14.1. It is temptingto write the first of the compound statements as follows (note the differences in the three substitution commands):

 /<[hH][123]>.*<\/[hH][123]>/ { ;# match whole heading

  h ;# save copy of pattern space

  s/^.*\(<[hH][123]>\)/\1/ ;# delete text before opening tag

  s/\(<\/[hH][123]>\).*$/\1/ ;# delete text after closing tag

  p ;# print what remains

  g ;# retrieve saved pattern space

  s/^.*<\/[hH][123]>// ;# delete through closing tag

  b top

Explain why this doesn't work. (Hint: Remember the difference between greedy and minimal matches [Example 14.53]. Sed lacks the latter.)

14.18  考虑 Perl 中的以下正则表达式:/^(?:((?:ab)+) |a((?:ba)*))$/。用英语描述它将匹配的字符串集。显示该集合的自然 NFA 以及最小 DFA。描述每个匹配字符串中应捕获的子字符串。基于此示例,讨论在 Perl 中使用 DFA 匹配字符串的实用性。

14.18  Consider the following regular expression in Perl: /^(?:((?:ab)+) |a((?:ba)*))$/. Describe, in English, the set of strings it will match. Show a natural NFA for this set, together with the minimal DFA. Describe the substrings that should be captured in each matching string. Based on this example, discuss the practicality of using DFAs to match strings in Perl.

14-02-9780124104099 14.19–14.21 更深入。

14.19–14.21  In More Depth.

14.7 探索

14.7 Explorations

14.22 了解本书使用的排版系统 T E X [ Knu86 ] 和 L A T E X [ Lam94 ]。探索其专门针对目标的方式领域——专业排版——影响了它的设计。您可能希望考虑的功能包括动态作用域、相对贫乏的算术和控制流功能以及使用宏作为基本控制抽象。

14.22 Learn about TEX [Knu86] and LATEX [Lam94], the typesetting system used to create this book. Explore the ways in which its specialized target domain—professional typesetting—influenced its design. Features you might wish to consider include dynamic scoping, the relatively impoverished arithmetic and control-flow facilities, and the use of macros as the fundamental control abstraction.

14.23 研究 JavaScript 和/或 Java 小程序的安全机制。程序到底可以做什么以及为什么?哪些可能有用的功能由于无法保证安全而未提供?提供的功能中还存在哪些潜在的安全漏洞

14.23 Research the security mechanisms of JavaScript and/or Java applets. What exactly are programs allowed to do and why? What potentially useful features have not been provided because they can't be made secure? What potential security holes remain in the features that are provided?

14.24 了解网络爬虫— 探索万维网的程序。编写一个搜索您感兴趣内容的爬虫。哪些语言特性或工具似乎对这项任务最有用?警告:自动网络爬虫是一项公共活动,受严格的礼仪规则约束。在创建爬虫之前,请先进行网络搜索并了解规则,并在将其放在本地子网(甚至您自己的机器)之外之前非常仔细地测试您的代码。特别要注意,对同一台服务器的快速请求构成拒绝服务攻击,这是一种潜在的刑事犯罪。

14.24 Learn about web crawlers—programs that explore the World Wide Web. Build a crawler that searches for something of interest. What language features or tools seem most useful for the task? Warning: Automated web crawling is a public activity, subject to strict rules of etiquette. Before creating a crawler, do a web search and learn the rules, and test your code very carefully before letting it outside your local subnet (or even your own machine). In particular, be aware that rapid-fire requests to the same server constitute a denial of service attack, a potentially criminal offense.

14.25 在侧边栏 14.9 中,我们指出awkegrep的“扩展”正则表达式通常通过先转换为 NFA,然后再转换为 DFA 来实现,而 Perl 及其同类的正则表达式通常通过回溯搜索来实现。一些工具(包括 GNU ggrep)使用 Boyer-Moore-Gosper 算法 [ BM77KMP77 ] 的变体来实现更快的确定性搜索。了解此算法的工作原理。它的优点是什么?它可以用于 Perl 等语言吗?

14.25 In Sidebar 14.9 we noted that the “extended” REs of awk and egrep are typically implemented by translating first to an NFA and then to a DFA, while those of Perl and its ilk are typically implemented via backtracking search. Some tools, including GNU ggrep, use a variant of the Boyer-Moore-Gosper algorithm [BM77, KMP77] for faster deterministic search. Find out how this algorithm works. What are its advantages? Could it be used in languages like Perl?

14.26 在侧边栏 14.10 中,我们指出,非常量模式通常必须在使用时重新编译。希望减少由此产生的开销的 Perl 程序员可以使用o尾随修饰符或qr引用运算符来禁止重新编译。研究这些机制对性能的影响。同时推测语言实现在多大程度上可以自动高效地确定何时应该进行重新编译。

14.26 In Sidebar 14.10 we noted that nonconstant patterns must generally be recompiled whenever they are used. Perl programmers who wish to reduce the resulting overhead can inhibit recompilation using the o trailing modifier or the qr quoting operator. Investigate the impact of these mechanisms on performance. Also speculate as to the extent to which it might be possible for the language implementation to determine, automatically and efficiently, when recompilation should occur.

14.27在 第 14.4.2 节中,我们对 Perl RE 的介绍并不完整。未介绍的功能包括前瞻和后瞻(上下文)断言、注释、修饰符的增量启用和禁用、嵌入代码、条件、Unicode 支持、非斜线分隔符和音译(tr///)运算符。了解这些功能的工作原理。解释它们是否(以及如何)扩展了符号的表达能力。如果没有这些功能,如何模拟它们(可能使用周围的 Perl 代码)?

14.27 Our coverage of Perl REs in Section 14.4.2 was incomplete. Features not covered include look-ahead and look-behind (context) assertions, comments, incremental enabling and disabling of modifiers, embedded code, conditionals, Unicode support, nonslash delimiters, and the transliteration (tr///) operator. Learn how these work. Explain if (and how) they extend the expressive power of the notation. How could each be emulated (possibly with surrounding Perl code) if it were not available?

14.28 研究 PHP、Tcl、Python、Ruby、JavaScript、Emacs Lisp、Java 和 C# 中 RE 支持的细节。写一篇论文,尽可能简洁地记录它们之间的差异,并使用 Perl 作为比较的参考。

14.28 Investigate the details of RE support in PHP, Tcl, Python, Ruby, JavaScript, Emacs Lisp, Java, and C#. Write a paper that documents, as concisely as possible, the differences among these, using Perl as a reference for comparison.

14.29 在网络上搜索 Perl 6,这是一项基于社区的努力,已经进行了多年。写一份报告总结变化,并附上尊重 Perl 5。您如何看待这些变化?如果您负责修订,您会做哪些不同的事情?

14.29 Do a web search for Perl 6, a community-based effort that has been in the works for many years. Write a report that summarizes the changes with respect to Perl 5. What do you think of these changes? If you were in charge of the revision, what would you do differently?

14.30 了解 AJAX,这是一组网络技术,允许 JavaScript 程序在“后台”与网络服务器交互,以便在初始加载后动态更新浏览器中的页面。您可以使用 AJAX 构建哪些类型的应用程序,而这些应用程序您无法轻松构建?JavaScript 的哪些功能对于使该技术发挥作用最重要?

14.30 Learn about AJAX, a collection ofweb technologies that allows a JavaScript program to interact with web servers “in the background,” to dynamically update a page in a browser after its initial loading. What kinds of applications can you build in AJAX that you couldn't easily build otherwise? What features of JavaScript are most important for making the technology work?

14.31 了解 Google 开发的一种语言 Dart。Dart 最初旨在作为 JavaScript 的后继者,现在仅支持将其作为开发将翻译JavaScript 的代码的语言。如何解释这种策略的改变?未来几年 JavaScript 出现其他竞争对手的可能性有多大?

14.31 Learn about Dart, a language developed at Google. Initially intended as a successor to JavaScript, Dart is now supported only as a language in which to develop code that will be translated into JavaScript. What explains the change in strategy? What are the odds that some other competitor to JavaScript will emerge in future years?

14-02-9780124104099 14.32–14.35 更深入。

14.32–14.35  In More Depth.

14.8 书目注释

14.8 Bibliographic Notes

大多数主流脚本语言及其前身都在语言设计者或其亲密同事的书中进行了描述:awk [ AKW88 ]、Perl [ CfWO12 ]、PHP [ TML13 ]、Python [ vRD11 ] 和 Ruby [ TFH13 ]。其中几本书有在线版本。大多数语言也在各种其他文本中进行了描述,并且大多数都有专门的网站:perl.org、php.netpython.orgruby-lang.org。许多机器预装了有关 Perl 的大量文档;输入man perl可查看索引。

Most of the major scripting languages and their predecessors are described in books by the language designers or their close associates: awk [AKW88], Perl [CfWO12], PHP [TML13], Python [vRD11], and Ruby [TFH13]. Several of these books have versions available on-line. Most of the languages are also described in a variety of other texts, and most have dedicated web sites: perl.org, php.net, python.org, ruby-lang.org. Extensive documentation for Perl is pre-installed on many machines; type man perl for an index.

Rexx [ Ame96a ] 由美国国家标准协会 (ANSI) 标准化。JavaScript [ ECM11 ] 由欧洲标准机构 ECMA 标准化。Guile ( gnu.org/software/guile/ ) 是 GNU 的 Scheme 脚本实现。万维网标准(包括 HTML5、XML、XSL、XPath 和 XSLT)由万维网联盟 ( www.w3.org ) 颁布。对于那些将页面更新为 HTML5 的用户来说, validator.w3.org上的验证服务特别有用。w3schools.com 上许多与 Web 相关的主题的高质量教程。

Rexx [Ame96a] was standardized by ANSI, the American National Standards Institute. JavaScript [ECM11] is standardized by ECMA, the European standards body. Guile (gnu.org/software/guile/) is GNU's Scheme implementation for scripting. Standards for the World Wide Web, including HTML5, XML, XSL, XPath, and XSLT, are promulgated by the World Wide Web Consortium: www.w3.org. For those updating their pages to HTML5, the validation service at validator.w3.org is particularly useful. High-quality tutorials on many web-related topics can be found at w3schools.com.

Hauben 和 Hauben [ HH97a ] 描述了互联网的历史根源,包括早期的 Unix 工作。关于各种 Unix shell 语言的原始文章包括 Mashey [ Mas76 ]、Bourne [ Bou78 ] 和 Korn [ Kor94 ] 的文章。关于 APL 的原始参考资料来自 Iverson [ Ive62 ]。Ousterhout [ Ous98 ] 介绍了脚本语言的一般情况,特别是 Tcl。Chonacky 和 ​​Winch [ CW05 ] 对 Maple、Mathematica 和 Matlab 进行了比较和对比。Richard Gabriel 的“Worse Is Better”论文集可以在www.dreamsongs.com/WorseIsBetter.html上找到。在 Abelson、Greenspun 和 Sandon 的在线Tcl for Web Nerds指南(philip.greenspun.com/tcl/index.adp)的介绍章节中可以找到类似的 Tcl 和 Scheme 的比较。

Hauben and Hauben [HH97a] describe the historical roots of the Internet, including early work on Unix. Original articles on the various Unix shell languages include those of Mashey [Mas76], Bourne [Bou78], and Korn [Kor94]. The original reference on APL is by Iverson [Ive62]. Ousterhout [Ous98] makes the case for scripting languages in general, and Tcl in particular. Chonacky and Winch [CW05] compare and contrast Maple, Mathematica, and Matlab. Richard Gabriel's collection of “Worse Is Better” papers can be found at www.dreamsongs.com/WorseIsBetter.html. A similar comparison of Tcl and Scheme can be found in the introductory chapter of Abelson, Greenspun, and Sandon's on-line Tcl for Web Nerds guide (philip.greenspun.com/tcl/index.adp).


1 Postscript 是 Adob​​e Systems, Inc. 开发的一种编程语言,用于描述图像和文档(我们将在 Sidebar 15.1 中再次讨论它)。封装 Postscript (EPS) 是 Postscript 的一种受限形式,用于嵌入到其他文档中的图形。可移植文档格式 (PDF,也是由 Adob​​e 开发的) 是一种独立的文件格式,它将 Postscript 的一个子集与字体嵌入和压缩机制结合在一起。从计算角度来看,它的功能不如 Postscript 强大,但可移植性更强,渲染速度更快、更容易。

1 Postscript is a programming language developed at Adobe Systems, Inc. for the description of images and documents (we consider it again in Sidebar 15.1). Encapsulated Postscript (EPS) is a restricted form of Postscript intended for figures that are to be embedded in other documents. Portable Document Format (PDF, also by Adobe) is a self-contained file format that combines a subset of Postscript with font embedding and compression mechanisms. It is strictly less powerful than Postscript from a computational perspective, but much more portable, and faster and easier to render.

2 Rexx 和 Tcl 具有面向对象的扩展,分别名为 Object Rexx 和 Incr Tcl。Perl 5 包含一些(相当笨拙的)面向对象功能;Perl 6 将具有更统一的对象支持。

2 Rexx and Tcl have object-oriented extensions, named Object Rexx and Incr Tcl, respectively. Perl 5 includes some (rather awkward) object-oriented features; Perl 6 will have more uniform object support.

3这里的括号很重要。中缀算术遵循传统的优先规则,但方法调用从左到右进行。同样,可以在参数列表周围省略括号,但方法选择点 (.) 比参数分隔逗号 (,) 分组更紧密,因此2.send '*', 4.send ' + ', 5 的计算结果为 18,而不是 13。

3 Parentheses here are significant. Infix arithmetic follows conventional precedence rules, but method invocation proceeds from left to right. Likewise, parentheses can be omitted around argument lists, but the method-selecting dot (.) groups more tightly than the argument-separating comma (,), so 2.send '*', 4.send ' + ', 5 evaluates to 18, not 13.

4术语“URI”通常与“URL”(统一资源定位符)互换使用,但万维网联盟区分了这两者。所有 URI 都是分层(多部分)名称。URL 是 URI 的一种;它们使用命名方案来指示在哪里可以找到资源。其他 URI 可以使用其他命名方案。

4 The term “URI” is often used interchangeably with “URL” (uniform resource locator), but the World Wide Web Consortium distinguishes between the two. All URIs are hierarchical (multipart) names. URLs are one kind of URIs; they use a naming scheme that indicates where to find the resource. Other URIs can use other naming schemes.

5一次性请求通常使用post类型表单。get类型表单看起来有点笨拙,因为参数明显嵌入在 URI 中,但这使其具有可重复性的优势:客户端浏览器可以将其“收藏”。

5 One typically uses post type forms for one-time requests. A get type form appears a little clumsier, because arguments are visibly embedded in the URI, but this gives it the advantage of repeatability: it can be “bookmarked” by client browsers.

6严格来说,] 和 } 不需要保护性反斜杠,除非前面有不匹配的(且不受保护的) [ 或 {。

6 Strictly speaking, ] and } don't require a protective backslash unless there is a preceding unmatched (and unprotected) [ or {, respectively.

7目前正在为 Perl 6 设计更广泛的功能,这里将不再介绍。

7 More extensive features, currently under design for Perl 6, will not be covered here.

8 Ruby 中privateprotected的含义与 C++、Java 或 C# 中的含义不同: Ruby 中的私有方法仅适用于对象的当前实例;而受保护的方法适用于当前类或其后代的任何实例。

8 The meanings of private and protected in Ruby are different from those in C++, Java, or C#: private methods in Ruby are available only to the current instance of an object; protected methods are available to any instance of the current class or its descendants.

进一步了解实现情况

A Closer Look at Implementation

进一步了解实现情况

A Closer Look at Implementation

在此,即本文的最后且最短的部分中,我们将焦点重新转移到实现问题上。

In this, the final and shortest of the major parts of the text, we return our focus to implementation issues.

第十五章 考虑了在语义分析之后必须完成的工作,以生成可运行的程序。本章的前半部分概括地描述了典型编译器的后端结构,调查了中间程序表示,并使用第 4 章的属性语法框架来描述编译器如何生成汇编级代码。本章的后半部分描述了典型进程地址空间的结构,并解释了汇编器链接器如何将编译器的输出转换为可执行代码。

Chapter 15 considers the work that must be done, in the wake of semantic analysis, to generate a runnable program. The first half of the chapter describes, in general terms, the structure of the back end of the typical compiler, surveys intermediate program representations, and uses the attribute grammar framework of Chapter 4 to describe how a compiler produces assembly-level code. The second halfofthe chapter describes the structure ofthe typical process address space, and explains how the assembler and linker transform the output of the compiler into executable code.

在任何非平凡的语言实现中,编译器都假设存在大量预先存在的代码,用于存储管理、异常处理、动态链接等。更复杂的语言可能还需要事件、线程和消息。当实现这些功能的库依赖于编译器的知识或正在运行的程序的结构时,它们就构成了运行时系统。我们将在第 16 章中讨论此类系统。我们特别关注虚拟机;机器代码的运行时操作;以及反射机制,这些机制允许程序推断其运行时结构和类型。

In any nontrivial language implementation, the compiler assumes the existence of a large body of preexisting code for storage management, exception handling, dynamic linking, and the like. A more sophisticated language may require events, threads, and messages as well. When the libraries that implement these features depend on knowledge of the compiler or of the structure of the running program, they are said to constitute a run-time system. We consider such systems in Chapter 16. We focus in particular on virtual machines; run-time manipulation of machine code; and reflection mechanisms, which allow a program to reason about its runtime structure and types.

第 15 章中的后端编译器描述必然过于简单。整本书和课程都致力于讲述更完整的故事,其中大部分都侧重于用于生成高效代码的代码改进优化技术。当前文本的第 17 章完全包含在配套网站上,概述了代码改进。由于大多数程序员永远不会编写编译器的后端,因此第 17 章的目标更多的是传达编译器的功能,而不是它如何执行。理解这些材料的程序员将能够更好地“使用”编译器,了解什么是可能的,在常见情况下会发生什么,以及如何避免难以优化的编程习语。主题包括本地和“全局”(过程级)冗余消除、数据流分析、循环优化和寄存器分配。

The back-end compiler description in Chapter 15 is by necessity simplistic. Entire books and courses are devoted to the fuller story, most of which focuses on the code improvement or optimization techniques used to produce efficient code. Chapter 17 of the current text, contained entirely on the companion site, provides an overview of code improvement. Since most programmers will never write the back end of a compiler, the goal of Chapter 17 is more to convey a sense of what the compiler does than exactly how it does it. Programmers who understand this material will be in a better position to “work with” the compiler, knowing what is possible, what to expect in common cases, and how to avoid programming idioms that are hard to optimize. Topics include local and “global” (procedure-level) redundancy elimination, data flow analysis, loop optimization, and register allocation.

15

构建可运行的程序

Building a Runnable Program

如第 1.6 节所述编译的各个阶段通常分为负责源代码分析的前端、负责目标代码合成的后端,以及通常负责独立于语言和机器的代码改进的“中端”。第 2 章和4章讨论了前端的工作,最终构建了语法树。本章将介绍后端的工作,特别是代码生成、汇编和链接。我们将在第17 章继续讨论代码改进。

As noted in Section 1.6, the various phases of compilation are commonly grouped into a front end responsible for the analysis of source code, a back end responsible for the synthesis of target code, and often a “middle end” responsible for language- and machine-independent code improvement. Chapters 2 and 4 discussed the work of the front end, culminating in the construction of a syntax tree. The current chapter turns to the work of the back end, and specifically to code generation, assembly, and linking. We will continue with code improvement in Chapter 17.

第 6 章第 10章中,我们经常讨论编译器为实现各种语言特性而生成的代码。现在我们将看看编译器如何根据语法树生成代码,以及如何将多次编译的输出组合起来生成可运行的程序。从第15.1 节开始,我们将比第 1 章更详细地概述程序合成的工作。我们特别关注将这项工作划分为阶段的几种合理方法中的一种。在第 15.2 节中,我们将考虑在这些阶段之间传递的中间代码的许多可能形式。在配套站点上,我们提供了两个具体示例的更详细信息——GNU 编译器使用的 GIMPLE 和 RTL 格式。我们将在第16 章中考虑另外两种中间形式:Java 字节码和 Microsoft 及公共语言基础结构的其他实现者使用的公共中间语言 (CIL)。

In Chapters 6 through 10, we often discussed the code that a compiler would generate to implement various language features. Now we will look at how the compiler produces that code from a syntax tree, and how it combines the output of multiple compilations to produce a runnable program. We begin in Section 15.1 with a more detailed overview of the work of program synthesis than was possible in Chapter 1. We focus in particular on one of several plausible ways of dividing that work into phases. In Section 15.2 we then consider the many possible forms of intermediate code passed between these phases. On the companion site we provide a bit more detail on two concrete examples—the GIMPLE and RTL formats used by the GNU compilers. We will consider two additional intermediate forms in Chapter 16: Java bytecode and the Common Intermediate Language (CIL) used by Microsoft and other implementors of the Common Language Infrastructure.

15.3 节中,我们讨论如何使用属性文法作为形式框架,从抽象语法树生成汇编代码。在15.4 节中,我们讨论二进制目标文件的内部组织和程序在内存中的布局。在15.5 节中,我们描述汇编。在 15.6 节中,我们讨论链接。

In Section 15.3 we discuss the generation of assembly code from an abstract syntax tree, using attribute grammars as a formal framework. In Section 15.4 we discuss the internal organization of binary object files and the layout of programs in memory. Section 15.5 describes assembly. Section 15.6 considers linking.

15.1 后端编译器结构

15.1 Back-End Compiler Structure

正如我们在第 4 章中提到的那样,后端编译器结构的统一性不如前端结构。即使是文本这样的非常规编译器处理器、源到源翻译器和 VLSI 布局工具必须扫描、解析和分析其输入的语义。然而,当涉及到后端时,即使是同一台机器上同一种语言的编译器也可能具有非常不同的内部结构。

As we noted in Chapter 4, there is less uniformity in back-end compiler structure than there is in front-end structure. Even such unconventional compilers as text processors, source-to-source translators, and VLSI layout tools must scan, parse, and analyze the semantics of their input. When it comes to the back end, however, even compilers for the same language on the same machine can have very different internal structure.

正如我们将在15.2 节中看到的,不同的编译器可能使用不同的中间形式在内部表示程序。它们在执行的代码改进形式上也可能有很大差异。简单的编译器或为编译速度而不是目标代码执行速度而设计的编译器(例如,“即时”编译器)可能根本不会做太多的改进。即时或“即装即用”编译器(将程序作为单个高级操作编译然后执行,而不将目标代码写入文件的编译器)可能不使用单独的链接器。在某些编译器中,大部分或全部代码生成器可能由以目标机器的正式描述作为输入的工具(“代码生成器”)自动编写。

As we shall see in Section 15.2, different compilers may use different intermediate forms to represent a program internally. They may also differ dramatically in the forms of code improvement they perform. A simple compiler, or one designed for speed of compilation rather than speed of target code execution (e.g., a “just-in-time” compiler) may not do much improvement at all. A just-in-time or “load-and-go” compiler (one that compiles and then executes a program as a single high-level operation, without writing the target code to a file) may not use a separate linker. In some compilers, much or all of the code generator may be written automatically by a tool (a “code generator generator”) that takes a formal description of the target machine as input.

15.1.1 一组合理的阶段

15.1.1 A Plausible Set of Phases

例 15.1

Example 15.1

编译阶段

Phases of compilation

图 15.1说明了传统编译器的合理七阶段结构。前三个阶段(扫描、解析和语义分析)依赖于语言;后两个阶段(目标代码生成和机器特定代码改进)依赖于机器,中间两个阶段(中间代码生成和机器无关代码改进)(初步估计)既不依赖于语言也不依赖于机器。扫描器和解析器驱动一组构建语法树的操作例程。语义分析器遍历树,执行所有静态语义检查并初始化后端使用的各种属性(主要是符号表指针和需要动态检查的指示)。■

Figure 15.1 illustrates a plausible seven-phase structure for a conventional compiler. The first three phases (scanning, parsing, and semantic analysis) are language dependent; the last two (target code generation and machine-specific code improvement) are machine dependent, and the middle two (intermediate code generation and machine-independent code improvement) are (to first approximation) dependent on neither the language nor the machine. The scanner and parser drive a set of action routines that build a syntax tree. The semantic analyzer traverses the tree, performing all static semantic checks and initializing various attributes (mainly symbol table pointers and indications of the need for dynamic checks) of use to the back end. ■

f15-01-9780124104099
图 15.1 一组合理的编译器阶段。这里我们展示了语义分析和中间代码生成之间的更明显的分离,这比我们在第 1 章中考虑的要多(见图1.3)。与机器无关的代码改进采用的中间形式类似于理想机器的汇编语言,该机器具有无限数量的寄存器。机器特定的代码改进(特别是寄存器分配和指令调度)采用目标机器的汇编语言。虚线显示了两遍编译器的前端和后端之间的公共“断点”。在某些实现中,与机器无关的代码改进可能位于单独的“中端”遍中。

虽然某些代码改进可以在语法树上执行,但程序的层次结构越少,大多数代码改进就越容易。因此,我们的示例编译器包含一个用于中间代码生成的显式阶段。代码生成器首先将树的节点分组为基本块,每个基本块都包含一组最大长度的操作,这些操作应在运行时按顺序执行,而没有进出分支。然后,它创建一个控制流图,其中节点是基本块,弧表示块间控制流。在每个基本块中,操作表示为具有无限数量寄存器的理想机器的指令。我们将这些称为虚拟寄存器。通过为每个计算值分配一个新的寄存器,编译器可以避免在编译过程的早期在原本独立的计算之间建立人为连接。

While certain code improvements can be performed on syntax trees, a less hierarchical representation of the program makes most code improvement easier. Our example compiler therefore includes an explicit phase for intermediate code generation. The code generator begins by grouping the nodes of the tree into basic blocks, each of which consists of a maximal-length set of operations that should execute sequentially at run time, with no branches in or out. It then creates a control flow graph in which the nodes are basic blocks and the arcs represent interblock control flow. Within each basic block, operations are represented as instructions for an idealized machine with an unlimited number of registers. We will call these virtual registers. By allocating a new one for every computed value, the compiler can avoid creating artificial connections between otherwise independent computations too early in the compilation process.

例 15.2

Example 15.2

GCD 程序抽象语法树(重演)

GCD program abstract syntax tree (reprise)

示例 1.20中,我们使用了一个简单的最大公约数 (GCD) 程序来说明编译的各个阶段。该程序的语法树如图1.6所示;此处将其复制(略有改动)为图 15.2。相应的控制流图如图15.3所示。我们将在第 15.3 节练习 15.6中讨论生成该图的技术。第 17 章将介绍控制流图的其他示例。■

In Example 1.20 we used a simple greatest common divisor (GCD) program to illustrate the phases of compilation. The syntax tree for this program appeared in Figure 1.6; it is reproduced here (in slightly altered form) as Figure 15.2. A corresponding control flow graph appears in Figure 15.3. We will discuss techniques to generate this graph in Section 15.3 and Exercise 15.6. Additional examples of control flow graphs will appear in Chapter 17. ■

f15-02-9780124104099
图 15.2 GCD 程序的语法树和符号表。与图 1.6的唯一区别是添加了显式空节点,以指示空参数列表并终止语句列表。
f15-03-9780124104099
图 15.3 GCD 程序的控制流图。基本块内的代码以侧边栏 5.1 中介绍的伪汇编符号显示,每个计算值都有一个不同的虚拟寄存器(此处称为 v1…v13)。寄存器 a1 和 rv 用于将值传递到子程序和从子程序传递值。

编译的与机器无关的代码改进阶段对控制流图执行各种转换。它修改每个基本块内的指令序列以消除冗余的加载、存储和算术计算;这是局部代码改进。它还识别并删除子程序内基本块之间边界上的各种冗余;这是全局代码改进。作为后者的一个例子,在if语句之前立即计算的表达式的值不需要在else之后的代码中重新计算。同样,如果出现在循环体内的表达式的值在后续迭代中不会改变,则只需求值一次。一些全局改进会改变基本块的数量和/或它们之间的弧。

The machine-independent code improvement phase of compilation performs a variety of transformations on the control flow graph. It modifies the instruction sequence within each basic block to eliminate redundant loads, stores, and arithmetic computations; this is local code improvement. It also identifies and removes a variety of redundancies across the boundaries between basic blocks within a subroutine; this is global code improvement. As an example of the latter, an expression whose value is computed immediately before an if statement need not be recomputed within the code that follows the else. Likewise an expression that appears within the body of a loop need only be evaluated once if its value will not change in subsequent iterations. Some global improvements change the number of basic blocks and/or the arcs among them.

值得注意的是,“全局”代码改进通常只考虑当前子程序,而不是整个程序。编译器技术的许多最新研究都针对“真正的全局”技术,称为过程间代码改进。由于程序员一般不愿意放弃单独编译(重新编译数十万行代码是一项非常耗时的操作),所以实用的过程间代码改进器必须在链接时完成大部分工作。要克服的(众多)挑战之一是开发一种分工和中间表示,使编译器能够在(单独)编译期间完成尽可能多的工作,但留下足够多的细节未确定,以便链接时代码改进器能够完成其工作。

It is worth noting that “global” code improvement typically considers only the current subroutine, not the program as a whole. Much recent research in compiler technology has been aimed at “truly global” techniques, known as interprocedural code improvement. Since programmers are generally unwilling to give up separate compilation (recompiling hundreds of thousands of lines of code is a very time-consuming operation), a practical interprocedural code improver must do much of its work at link time. One of the (many) challenges to be overcome is to develop a division of labor and an intermediate representation that allow the compiler to do as much work as possible during (separate) compilation, but leave enough of the details undecided that the link-time code improver is able to do its job.

在完成独立于机器的代码改进之后,编译的下一个阶段是目标代码生成。此阶段将基本块串在一起形成一个线性程序,将每个块转换为目标机器的指令集,并生成与控制流图弧相对应的分支指令(或“fall-through”)。此阶段的输出与实际汇编语言的主要区别在于它继续依赖虚拟寄存器。只要中间形式的伪指令与目标机器的伪指令相当接近,这个编译阶段虽然很繁琐,但或多或​​少还是很简单的。

Following machine-independent code improvement, the next phase of compilation is target code generation. This phase strings the basic blocks together into a linear program, translating each block into the instruction set of the target machine and generating branch instructions (or “fall-throughs”) that correspond to the arcs of the control flow graph. The output of this phase differs from real assembly language primarily in its continued reliance on virtual registers. So long as the pseudoinstructions of the intermediate form are reasonably close to those of the target machine, this phase of compilation, though tedious, is more or less straightforward.

为了减少程序员的工作量并提高将编译器移植到新目标机器的难度,有时会根据机器的正式描述自动生成目标代码生成器。自动生成的代码生成器都依赖于某种模式匹配算法,用等效的目标机器指令序列替换中间代码指令序列。本章末尾的参考书目注释中可以找到对几种此类算法的引用;详细信息超出了本书的范围。

To reduce programmer effort and increase the ease with which a compiler can be ported to a new target machine, target code generators are sometimes generated automatically from a formal description of the machine. Automatically generated code generators all rely on some sort of pattern-matching algorithm to replace sequences of intermediate code instructions with equivalent sequences of target machine instructions. References to several such algorithms can be found in the Bibliographic Notes at the end of this chapter; details are beyond the scope of this book.

我们示例编译器结构的最后阶段包括寄存器分配和指令调度,这两者都可以视为特定于机器的代码改进。寄存器分配要求我们将早期阶段使用的无限虚拟寄存器映射到目标机器中可用的有界架构寄存器集上。如果没有足够的架构寄存器可供使用,我们可能需要生成额外的加载和存储,以在两个或多个虚拟寄存器之间多路复用给定的架构寄存器。指令调度(在 C-5.5 和 C-17.6 节中描述)包括重新排序每个基本块的指令,以尝试填充目标机器的管道。

The final phase of our example compiler structure consists of register allocation and instruction scheduling, both of which can be thought of as machine-specific code improvement. Register allocation requires that we map the unlimited virtual registers employed in earlier phases onto the bounded set of architectural registers available in the target machine. If there aren't enough architectural registers to go around, we may need to generate additional loads and stores to multiplex a given architectural register among two or more virtual registers. Instruction scheduling (described in Sections C-5.5 and C-17.6) consists of reordering the instructions of each basic block in an attempt to fill the pipeline(s) of the target machine.

15.1.2 阶段和过程

15.1.2 Phases and Passes

第 1.6 节中,我们将编译过程定义为相对于其余编译过程序列化的一个阶段或一系列阶段:它直到前面的阶段完成后才开始,并且在任何后续阶段开始之前完成。如果需要,可以将过程编写为单独的程序,从文件读取其输入并将其输出写入文件。两遍编译器特别常见。它们可以分为语义分析和中间代码生成,或中间代码生成和独立于机器的代码改进。在任一情况下,第一遍通常称为“前端”,第二遍称为“后端”。

In Section 1.6 we defined a pass of compilation as a phase or sequence of phases that is serialized with respect to the rest of compilation: it does not start until previous phases have completed, and it finishes before any subsequent phases start. If desired, a pass may be written as a separate program, reading its input from a file and writing its output to a file. Two-pass compilers are particularly common. They may be divided between semantic analysis and intermediate code generation or between intermediate code generation and machine-independent code improvement. In either case, the first pass is commonly referred to as the “front end” and the second pass as the “back end.”

与大多数编译器一样,我们的示例生成符号汇编语言作为其输出(一些编译器,包括 IBM 为 Power 系列编写的编译器,直接生成二进制机器代码)。汇编器(图 15.1中未显示)充当额外通道,为数据和代码片段分配地址,并将符号操作转换为其二进制编码。在大多数情况下,编译器的输入将由单个编译单元的源代码组成。汇编后,输出需要链接应用程序的其他片段和各种预先存在的子例程库。某些链接工作可能会延迟到加载时(程序执行之前)或甚至到运行时(程序执行期间)。我们将在15.515.7节中讨论汇编和链接。

Like most compilers, our example generates symbolic assembly language as its output (a few compilers, including those written by IBM for the Power family, generate binary machine code directly). The assembler (notshown in Figure 15.1) behaves as an extra pass, assigning addresses to fragments of data and code, and translating symbolic operations into their binary encodings. In most cases, the input to the compiler will have consisted of source code for a single compilation unit. After assembly, the output will need to be linked to other fragments of the application, and to various preexisting subroutine libraries. Some of the work of linking may be delayed until load time (immediately prior to program execution) or even until run time (during program execution). We will discuss assembly and linking in Sections 15.5 through 15.7.

15.2 中间形式

15.2 Intermediate Forms

中间形式(IF)提供了与机器无关的代码改进阶段之间的连接,并在各个后端阶段继续表示程序。

An intermediate form (IF) provides the connection between phases of machine-independent code improvement, and continues to represent the program during the various back-end phases.

IF 可以根据其级别或机器依赖程度进行分类。高级 IF 通常基于树或有向无环图 (DAG),它们直接捕捉现代编程语言的层次结构。高级 IF 有助于某些类型的独立于机器的代码改进、增量程序更新(例如,在基于语言的编辑器中)和直接解释(大多数解释器都使用基于树的内部 IF)。由于树的允许结构可以通过一组产生式来正式描述(如第 4.6 节所述),因此可以将基于树的形式的操作写成属性语法。

IFs can be classified in terms of their level, or degree of machine dependence. High-level IFs are often based on trees or directed acyclic graphs (DAGs) that directly capture the hierarchical structure of modern programming languages. A high-level IF facilitates certain kinds of machine-independent code improvement, incremental program updates (e.g., in a language-based editor), and direct interpretation (most interpreters employ a tree-based internal IF). Because the permissible structure of a tree can be described formally by a set of productions (as described in Section 4.6), manipulations of tree-based forms can be written as attribute grammars.

最常见的中级 IF 采用三地址指令来处理简单的理想化机器,通常是具有无限数量的寄存器的机器。这些指令通常嵌入在控制流图中。由于典型的指令指定两个操作数、一个运算符和一个目标,因此三地址指令有时被称为四重指令。低级 IF 通常类似于某些特定目标机器的汇编语言,通常是目标代码将在其上执行的物理机器。

The most common medium-level IFs employ three-address instructions for a simple idealized machine, typically one with an unlimited number of registers. Often the instructions are embedded in a control flow graph. Since the typical instruction specifies two operands, an operator, and a destination, three-address instructions are sometimes called quadruples. Low-level IFs usually resemble the assembly language of some particular target machine, most often the physical machine on which the target code will execute.

例 15.3

Example 15.3

图 15.1中的中间形式

Intermediate forms in Figure 15.1

不同的编译器使用不同的 IF。许多编译器内部使用多个 IF,但在常见的两遍组织中,其中一个 IF 被区分为“中间”形式,因为它是前端和后端之间外部可见的连接。在第 15.1.1 节的示例中,从语义分析传递到中间代码生成的语法树构成了高级 IF。包含伪汇编语言的控制流图(传入和传出与机器无关的代码改进)是中级 IF。目标机器的汇编语言(最初使用虚拟寄存器;后来使用架构寄存器)用作低级 IF。

Different compilers use different IFs. Many compilers use more than one IF internally, though in the common two-pass organization one of these is distinguished as “the” intermediate form by virtue of being the externally visible connection between the front end and the back end. In the example of Section 15.1.1, the syntax trees passed from semantic analysis to intermediate code generation constitute a high-level IF. Control flow graphs containing pseudo-assembly language (passed in and out of machine-independent code improvement) are a medium-level IF. The assembly language of the target machine (initially with virtual registers; later with architectural registers) serves as a low-level IF.

当然,“高级”、“中级”和“低级”IF 之间的区别有些任意:合理的设计空间非常大,从抽象到机器相关几乎是连续的。■

The distinction between “high-,” “medium-,” and “low-level” IFs is of course somewhat arbitrary: the plausible design space is very large, with a nearly continuous spectrum from abstract to machine-dependent. ■

具有针对多种不同目标架构的后端的编译器倾向于在高级或中级 IF 上完成尽可能多的工作,以便代码改进器中与机器无关的部分可以由不同的后端共享。相比之下,一些(但不是全部)为单一架构生成代码的编译器在相对低级的 IF 上执行大部分代码改进,这些 IF 紧密模仿目标机器的汇编语言。

Compilers that have back ends for several different target architectures tend to do as much work as possible on a high- or medium-level IF, so that the machine-independent parts of the code improver can be shared by different back ends. By contrast, some (but not all) compilers that generate code for a single architecture perform most code improvement on a comparatively low-level IF, closely modeled after the assembly language of the target machine.

在多语言编译器系列中,独立于源语言和目标机器的 IF 允许希望在m台机器上销售n种语言的编译器的软件供应商只构建n 个前端和m 个后端,而不是n × m 个集成编译器。即使在单语言编译器系列中,通用的、可能依赖于语言的 IF 也可以通过隔离需要更改的代码来简化移植到新机器的任务。在丰富的程序开发环境中,除了编译器的传递之外,可能还有各种工具可以理解和操作 IF。示例包括编辑器、汇编器、链接器、调试器、漂亮打印机和版本管理软件。在能够进行过程间(整个程序)代码改进的语言系统中,单独编译的模块和库可能只编译为 IF,而不是目标语言,将编译的最后阶段留给链接器。

In a multilanguage compiler family, an IF that is independent of both source language and target machine allows a software vendor who wishes to sell compilers for n languages on m machines to build just n front ends and m back ends, rather than n × m integrated compilers. Even in a single-language compiler family, a common, possibly language-dependent IF simplifies the task of porting to a new machine by isolating the code that needs to be changed. In a rich program development environment, there may be a variety of tools in addition to the passes of the compiler that understand and operate on the IF. Examples include editors, assemblers, linkers, debuggers, pretty-printers, and version-management software. In a language system capable of interprocedural (whole-program) code improvement, separately compiled modules and libraries may be compiled only to the IF, rather than the target language, leaving the final stages of compilation to the linker.

要存储在文件中,IF 需要线性表示。三地址指令序列自然是线性的。基于树的 IF 可以通过有序遍历进行线性化。控制流图等结构可以通过用相对于文件开头的索引替换指针来进行线性化。

To be stored in a file, an IF requires a linear representation. Sequences of three-address instructions are naturally linear. Tree-based IFs can be linearized via ordered traversal. Structures like control flow graphs can be linearized by replacing pointers with indices relative to the beginning of the file.

15.2.1 GIMPLE 和 RTL

15.2.1 GIMPLE and RTL

许多读者都熟悉 gcc 编译器。gcc 由自由软件基金会以开源形式发布,学术界和工业界都得到广泛应用。标准发布版包括 C、C++、Objective-C、Ada、Fortran、Go 和 Java 的前端。其他语言的前端(包括 Cobol、Modula-2 和 3、Pascal 和 PL/I)可单独购买。C 编译器是最初的编译器,也是使用最广泛的编译器(gcc最初代表“GNU C 编译器”)。它有适用于数十种处理器架构的后端,包括所有具有商业意义的选项。还有不基于gcc的 GNU 实现,适用于大约二十多种其他语言。

Many readers will be familiar with the gcc compilers. Distributed as open source by the Free Software Foundation, gcc is used very widely in both academia and industry. The standard distribution includes front ends for C, C++, Objective-C, Ada, Fortran, Go, and Java. Front ends for additional languages, including Cobol, Modula-2 and 3, Pascal and PL/I, are separately available. The C compiler is the original, and the one most widely used (gcc originally stood for “GNU C compiler”). There are back ends for dozens of processor architectures, including all commercially significant options. There are also GNU implementations, not based on gcc, for some two dozen additional languages.

15-02-9780124104099 更深入地

IN MORE DEPTH

Gcc有三个主要的 IF。大多数(特定于语言的)前端在内部使用高级语法树形式的某种变体,称为 GENERIC。机器独立代码改进的早期阶段使用一种稍低级的树形式,称为 GIMPLE(仍然是高级 IF)。后期阶段使用一种线性形式,称为 RTL(寄存器传输语言)。RTL 是一种中级 IF,但比大多数 IF 的级别要高一点:它在一系列伪指令上覆盖了一个控制流图。多年来,RTL 一直是gcc的主要 IF。GIMPLE于 2005 年推出,作为一种更适合机器独立代码改进的形式。我们在配套网站上更详细地讨论了 GIMPLE 和 RTL。

Gcc has three main IFs. Most of the (language-specific) front ends employ, internally, some variant of a high-level syntax tree form known as GENERIC. Early phases of machine-independent code improvement use a somewhat lower-level tree form known as GIMPLE (still a high-level IF). Later phases use a linear form known as RTL (register transfer language). RTL is a medium-level IF, but a bit higher level than most: it overlays a control flow graph on of a sequence of pseudoinstructions. RTL was, for many years, the principal IF for gcc. GIMPLE was introduced in 2005 as a more suitable form for machine-independent code improvement. We consider GIMPLE and RTL in more detail on the companion site.

15.2.2 基于堆栈的中间形式

15.2.2 Stack-Based Intermediate Forms

在简单和简洁至关重要的情况下,设计人员通常会求助于基于堆栈的语言。这种语言中的操作会从一个公共隐式堆栈中弹出参数并将结果推送到该堆栈中。没有命名操作数意味着基于堆栈的语言可以非常紧凑。在某些 HP 计算器中(练习 4.7),基于堆栈的表达式求值可最大限度地减少输入方程式所需的击键次数。对于嵌入式设备和打印机,Forth 和 Postscript 中基于堆栈的求值分别可减少内存和带宽要求(参见边栏 15.1)。

In situations where simplicity and brevity are paramount, designers often turn to stack-based languages. Operations in a such a language pop arguments from—and push results to—a common implicit stack. The lack of named operands means that a stack-based language can be very compact. In certain HP calculators (Exercise 4.7), stack-based expression evaluation serves to minimize the number of keystrokes required to enter equations. For embedded devices and printers, stack-based evaluation in Forth and Postscript serves to reduce memory and bandwidth requirements, respectively (see Sidebar 15.1).

当将代码从编译器传递到解释器或虚拟机时,基于堆栈的中级中间语言同样具有吸引力。四十年以前,P 代码(示例 1.15)使 Pascal 很容易移植到新机器上,并有助于加速该语言的采用。今天,Java 字节码的紧凑性有助于最大限度地缩短小程序的下载时间。通用中间语言 (CIL) 是 .NET 和公共语言基础结构 (CLI) 其他实现的 Java 字节码的类似物,同样紧凑且独立于机器。截至 2015 年,.NET 仅在 x86 和 ARM 上运行,但开源 Mono CLI 可用于所有主要指令集。我们将在第 16 章中详细讨论 Java 字节码和 CIL 。

Medium-level stack-based intermediate languages are similarly attractive when passing code from a compiler to an interpreter or virtual machine. Forty years ago, P-code (Example 1.15) made it easy to port Pascal to new machines, and helped to speed the language's adoption. Today, the compactness of Java bytecode helps minimize the download time for applets. Common Intermediate Language (CIL), the analogue of Java bytecode for .NET and other implementations of the Common Language Infrastructure (CLI), is similarly compact and machine independent. As of 2015, .NET runs only on the x86 and ARM, but the open-source Mono CLI is available for all the major instruction sets. We will consider Java bytecode and CIL in some detail in Chapter 16.

不幸的是,基于堆栈的 IF 并不适合许多代码改进技术:它限制了通过重新排序计算来消除冗余或提高管道性能的能力。因此,Java 字节码和 CIL 等语言往往主要用作外部格式,而不是编译器内代码的表示。

Unfortunately, stack-based IF is not well suited to many code improvement techniques: it limits the ability to eliminate redundancy or improve pipeline performance by reordering calculations. For this reason, languages like Java bytecode and CIL tend to be used mainly as an external format, not as a representation for code within a compiler.

例 15.4

Example 15.4

计算海伦公式

Computing Heron's formula

在许多情况下,与相应的三地址代码相比,基于堆栈的表达式代码占用的字节更少,但指定的指令更多。作为一个具体的例子,考虑 Heron 公式,该公式在给定三角形边长abc 的情况下计算三角形面积:

In many cases, stack-based code for an expression will occupy fewer bytes, but specify more instructions, than corresponding three-address code. As a concrete example, consider Heron's formula to compute the area of a triangle given the lengths of its sides, a, b, and c:

一个=ss一个sbs在哪里s=一个+b+2

A=s(sa)(sb)(sc),wheres=a+b+c2

si1_e

图 15.4比较了此公式的字节码和三地址版本。每行代表一条指令。如果我们假设abcs都是当前子例程的前几个局部变量,那么 Java 虚拟机 (JVM) 和 CLI 都可以使用单字节指令将它们移入或移出堆栈。因此,左列中倒数第二条指令是唯一需要多于一个字节的指令(它需要三个:一个用于两个指令用于指定push操作,以及两个用于指定sqrt例程的指令)。这样我们就得到了总共 23 条指令,占用 25 个字节。

Figure 15.4 compares bytecode and three-address versions of this formula. Each line represents a single instruction. If we assume that a, b, c, and s are all among the first few local variables of the current subroutine, both the Java Virtual Machine (JVM) and the CLI will be able to move them to or from the stack with single-byte instructions. Consequently, the second-to-last instruction in the left column is the only one that needs more than a single byte (it takes three: one for the push operation and two to specify the sqrt routine). This gives us a total of 23 instructions in 25 bytes.

f15-04-9780124104099
图 15.4 基于堆栈的 IF 与三地址 IF。显示了使用 Heron 公式计算三角形面积的两种代码版本。左侧是 Java 字节码或 CLI 通用中间语言的程式化版本。右侧是具有三地址指令的机器的相应伪汇编程序。字节码需要更多指令,但占用的空间较少。

设计与实现

Design & Implementation

15.1 后记

15.1 Postscript

如今,基于堆栈的语言最普遍的用途之一是文档准备。许多文档编译器(T E X、Microsoft Word 等)生成 Postscript 或相关的可移植文档格式 (PDF) 作为其目标语言。(大多数文档编译器也使用一些专用中间语言,并且具有多个后端,因此它们可以生成多种目标语言。)

One of the most pervasive uses of stack-based languages today occurs in document preparation. Many document compilers (TEX, Microsoft Word, etc.) generate Postscript or the related Portable Document Format (PDF) as their target language. (Most document compilers employ some special-purpose intermediate language as well, and have multiple back ends, so they can generate multiple target languages.)

Postscript 是基于堆栈的。它可移植、紧凑且易于生成。它还用 ASCII 编写,因此人类可以读取(尽管有些困难)。大多数专业级打印机都嵌入了 Postscript 解释器。代码改进的问题相对不重要:打印所需的大部分时间都花在了网络延迟、机械纸张传输和嵌入在(优化的)库例程中的数据操作上;解释时间很少成为瓶颈。另一方面,紧凑性至关重要,因为它会导致网络延迟。

Postscript is stack-based. It is portable, compact, and easy to generate. It is also written in ASCII, so it can be read (albeit with some difficulty) by human beings. Postscript interpreters are embedded in most professional-quality printers. Issues of code improvement are relatively unimportant: most of the time required for printing is consumed by network delays, mechanical paper transport, and data manipulations embedded in (optimized) library routines; interpretation time is seldom a bottleneck. Compactness on the other hand is crucial, because it contributes to network delays.

相比之下,相同公式的三地址代码将abcs保存在寄存器中,并且只需要 13 条指令。不幸的是,在典型的表示法中,除最后一条指令外,每条指令的长度都是 4 个字节(最后一条指令的长度是 8 个字节),而我们的 13 条指令将占用 56 个字节。■

By contrast, three-address code for the same formula keeps a, b, c, and s in registers, and requires only 13 instructions. Unfortunately, in typical notation each instruction but the last will be four bytes in length (the last will be eight), and our 13 instructions will occupy 56 bytes. ■

15.3 代码生成

15.3 Code Generation

例 15.5

Example 15.5

更简单的编译器结构

Simpler compiler structure

图 15.1的后端结构过于复杂,无法在一章中详细介绍。为了限制讨论范围,我们将在本章中满足于生成正确但简单的代码。这种选择将使我们能够考虑一个明显更简单的中端和后端。从图 15.1的结构开始,我们放弃与机器无关的代码改进器,然后将中间代码和目标代码生成合并为一个阶段。这个合并阶段生成纯线性汇编语言;因为我们不进行改变程序控制流的代码改进,所以不需要在控制流图中明确表示该流。我们还采用了一种更简单的寄存器分配算法,该算法可以在代码生成之前直接对语法树进行操作,从而无需虚拟寄存器以及随后映射到架构寄存器。最后,我们放弃指令调度。生成的编译器结构如图15.5所示。其代码生成阶段与图 15.1的中间代码生成非常相似。■

The back-end structure of Figure 15.1 is too complex to present in any detail in a single chapter. To limit the scope of our discussion, we will content ourselves in this chapter with producing correct but naive code. This choice will allow us to consider a significantly simpler middle and back end. Starting with the structure of Figure 15.1, we drop the machine-independent code improver and then merge intermediate and target code generation into a single phase. This merged phase generates pure, linear assembly language; because we are not performing code improvements that alter the program's control flow, there is no need to represent that flow explicitly in a control flow graph. We also adopt a much simpler register allocation algorithm, which can operate directly on the syntax tree prior to code generation, eliminating the need for virtual registers and the subsequent mapping onto architectural registers. Finally, we drop instruction scheduling. The resulting compiler structure appears in Figure 15.5. Its code generation phase closely resembles the intermediate code generation of Figure 15.1. ■

f15-05-9780124104099
图 15.5 更简单的非优化编译器结构(假设于 第 15.3 节中)。目标代码生成阶段与图 15.1中的中间代码生成阶段非常相似。

15.3.1 属性文法示例

15.3.1 An Attribute Grammar Example

与语义分析一样,中间代码生成可以用属性语法形式化,尽管它最常通过手写语法树的临时遍历来实现。为了清楚起见,我们在这里介绍一种属性语法。

Like semantic analysis, intermediate code generation can be formalized in terms of an attribute grammar, though it is most commonly implemented via handwritten ad hoc traversal of a syntax tree. We present an attribute grammar here for the sake of clarity.

图 1.7中,我们展示了 GCD 程序的简单 x86 汇编语言。我们将使用属性语法示例在此处生成类似的版本,但适用于 RISC 类机器,并使用伪汇编符号。由于此符号现在旨在表示目标代码,而不是中级或低级中间代码,因此我们将假设一个固定的、有限的寄存器集,让人联想到真实机器。我们将保留几个寄存器(a1a2sprv )用于特殊目的;其他寄存器(r1..rk)将可用于临时值和表达式求

In Figure 1.7, we presented naive x86 assembly language for the GCD program. We will use our attribute grammar example to generate a similar version here, but for a RISC-like machine, and in pseudo-assembly notation. Because this notation is now meant to represent target code, rather than medium- or low-level intermediate code, we will assume a fixed, limited register set reminiscent of real machines. We will reserve several registers (a1, a2, sp, rv) for special purposes; others (r1 .. rk) will be available for temporary values and expression evaluation.

例 15.6

Example 15.6

用于代码生成的属性语法

An attribute grammar for code generation

图 15.6包含我们的属性文法的一个片段。为了节省空间,我们只显示了图 15.2中实际出现的那些产生式。与第 4 章一样,产生式左侧的while : stmt之类的符号表示语法树中的while节点是几种stmt节点中的一种;它可以充当其父产生式右侧的stmt 。在我们的属性语法片段中, programexprstmt都有一个包含一系列指令的合成属性code 。 Program具有从编译器命令行获取的字符串类型的继承属性name 。 Id具有指向标识符的符号表条目的合成属性stp 。 Expr具有合成属性reg 指示在运行时将保存计算表达式的值的寄存器。Exprstmt具有继承属性next_free_reg,指示在评估给定表达式或语句之前立即可用的下一个寄存器(在临时变量的有序集合中)(即,在运行时不会保存任何有用的值)。 (为简单起见,我们将把寄存器当成堆栈来管理;有关更多信息请参见15.3.2 节。)■

Figure 15.6 contains a fragment of our attribute grammar. To save space, we have shown only those productions that actually appear in Figure 15.2. As in Chapter 4, notation like while : stmt on the left-hand side of a production indicates that a while node in the syntax tree is one of several kinds of stmt node; it may serve as the stmt in the right-hand side of its parent production. In our attribute grammar fragment, program, expr, and stmt all have a synthesized attribute code that contains a sequence of instructions. Program has an inherited attribute name of type string, obtained from the compiler command line. Id has a synthesized attribute stp that points to the symbol table entry for the identifier. Expr has a synthesized attribute reg that indicates the register that will hold the value of the computed expression at run time. Expr and stmt have an inherited attribute next_free_reg that indicates the next register (in an ordered set of temporaries) that is available for use (i.e., that will hold no useful value at run time) immediately before evaluation of a given expression or statement. (For simplicity, we will be managing registers as if they were a stack; more on this in Section 15.3.2.) ■

f15-06-9780124104099f15-07-9780124104099
图 15.6 属性语法从语法树生成代码。方括号界定单个目标指令。并列表示指令内的串联;“ + ”运算符表示指令列表的串联。handle.op 宏在三个属性规则中使用。

由于我们在示例中使用了符号表,并且符号表位于正式属性语法框架之外,因此我们必须在属性语法中添加一些额外的代码来进行存储管理。具体来说,在评估图 15.6的属性规则之前,我们必须遍历符号表,以便计算局部变量和参数(其中两个ij出现在 GCD 程序中)的堆栈框架偏移量,并生成汇编程序指令来为全局变量(我们的程序没有)分配空间。存储分配和其他汇编程序指令将在15.5 节中详细讨论。

Because we use a symbol table in our example, and because symbol tables lie outside the formal attribute grammar framework, we must augment our attribute grammar with some extra code for storage management. Specifically, prior to evaluating the attribute rules of Figure 15.6, we must traverse the symbol table in order to calculate stack-frame offsets for local variables and parameters (two of which—i and j—occur in the GCD program) and in order to generate assembler directives to allocate space for global variables (of which our program has none). Storage allocation and other assembler directives will be discussed in more detail in Section 15.5.

15.3.2 寄存器分配

15.3.2 Register Allocation

例 15.7

Example 15.7

基于堆栈的寄存器分配

Stack-based register allocation

属性语法规则本身的评估包括两个主要任务。在每个子树中,我们首先确定运行时用于保存各种数量的寄存器;然后生成代码。我们的简单寄存器分配策略使用next_free_reg继承属性将寄存器r1…r k作为表达式求值堆栈进行管理。例如,要计算(a + b) × (c − (d / e))的值,我们将生成以下内容:

Evaluation of the rules of the attribute grammar itself consists of two main tasks. In each subtree we first determine the registers that will be used to hold various quantities at run time; then we generate code. Our naive register allocation strategy uses the next_free_reg inherited attribute to manage registers r1…rk as an expression evaluation stack. To calculate the value of (a + b) × (c − (d / e)), for example, we would generate the following:

r1:=a–– 推
r2:=b–– 推 b
r1:= r1 + r2- 添加
r2:=c–– 推 c
r3:=d–– 推 d
r4:= e–– 按 e
r3:=r3/r4–– 划分
r2 := r2 − r3–– 减去
r1 := r1 × r2–– 乘以

在产生式id : expr →中,我们会在“堆栈”中分配下一个寄存器,图片其中我们使用expr.next_free_reg来索引reg_names(临时寄存器名称数组),并在宏handle_op中增加next_free_reg以使该寄存器在评估右侧操作数时不可用。无需明确“弹出”寄存器堆栈;当属性评估器返回到父节点并使用父节点(未修改的)next_free_reg属性时,这会自动发生。在我们的示例语法中,左侧操作数是在评估其他任何内容时唯一绑定寄存器的构造。在更完整的语法中,寄存器的其他长期使用可能出现在for循环之类的构造中(用于步长、索引和边界)。

Allocation of the next register on the “stack” occurs in the production id : expr, where we use expr.next_free_reg to index into reg_names, the array of temporary register names, and in macro handle_op, where we increment next_free_reg to make this register unavailable during evaluation of the right-hand operand. There is no need to “pop” the “register stack” explicitly; this happens automatically when the attribute evaluator returns to a parent node and uses the parent's (unmodified) next_free_reg attribute. In our example grammar, left-hand operands are the only constructs that tie up a register during the evaluation of anything else. In a more complete grammar, other long-term uses of registers would probably occur in constructs like for loops (for the step size, index, and bound).

在一段特别复杂的代码中,可能会用尽架构寄存器。在这种情况下,我们必须将一个或多个寄存器溢出到内存中。我们的简单寄存器分配器将寄存器推送到程序的子例程调用堆栈上,重新使用该寄存器用于其他目的,然后在再次需要之前将保存的值弹出回寄存器中。实际上,架构寄存器保存了表达式求值堆栈的前k 个元素,该堆栈的大小实际上不受限制。■

In a particularly complicated fragment of code it is possible to run out of architectural registers. In this case we must spill one or more registers to memory. Our naive register allocator pushes a register onto the program's subroutine call stack, reuses the register for another purpose, and then pops the saved value back into the register before it is needed again. In effect, architectural registers hold the top k elements of an expression evaluation stack of effectively unlimited size. ■

需要强调的是,我们的寄存器分配算法虽然正确,但对机器资源的利用率很低。我们没有尝试重新组织表达式以尽量减少使用的寄存器数量,也没有尝试将常用变量长时间保存在寄存器中(避免加载和存储)。如果我们生成的是中级中间代码,而不是目标代码,我们将使用虚拟寄存器,而不是架构寄存器,并且每次需要时都会分配一个新的寄存器,永远不会重用一个寄存器来保存不同的值。虚拟寄存器到架构寄存器的映射将在编译过程的后期进行。

It should be emphasized that our register allocation algorithm, while correct, makes very poor use of machine resources. We have made no attempt to reorganize expressions to minimize the number of registers used, or to keep commonly used variables in registers over extended periods of time (avoiding loads and stores). If we were generating medium-level intermediate code, instead of target code, we would employ virtual registers, rather than architectural ones, and would allocate a new one every time we needed it, never reusing one to hold a different value. Mapping of virtual registers to architectural registers would occur much later in the compilation process.

例 15.8

Example 15.8

GCD 程序目标代码

GCD program target code

GCD 程序的目标代码如图15.7所示。前几行是在符号表遍历期间生成的,在属性求值之前。属性program.name可能会传递给汇编程序,以告诉它要将可运行程序放入的文件的名称。真正的编译器可能还会生成汇编程序指令,以将符号表信息嵌入目标程序中。如图1.7所示,我们的代码质量非常差。我们将调查在第 17 章中,我们将介绍改进该算法的技术。在本章的剩余部分中,我们将讨论汇编和链接。■

Target code for the GCD program appears in Figure 15.7. The first few lines are generated during symbol table traversal, prior to attribute evaluation. Attribute program.name might be passed to the assembler, to tell it the name of the file into which to place the runnable program. A real compiler would probably also generate assembler directives to embed symbol-table information in the target program. As in Figure 1.7, the quality of our code is very poor. We will investigate techniques to improve it in Chapter 17. In the remaining sections of the current chapter we will consider assembly and linking. ■

f15-08-9780124104099
图 15.7 GCD 程序的目标代码使用图 15.6的属性语法,从图 15.2的语法树生成。

15-01-9780124104099检查你的理解

Check Your Understanding

1. 什么是代码生成器?它为什么有用?

1. What is a code generator generator? Why might it be useful?

2. 什么是基本块控制流程图

2. What is a basic block? A control flow graph?

3. 什么是虚拟寄存器?它们有什么用途?

3. What are virtual registers? What purpose do they serve?

4. 局部代码改进和全局代码改进有什么区别?

4. What is the difference between local and global code improvement?

5. 什么是寄存器溢出

5. What is register spilling?

6. 解释一下中级(IF)的“级别”是什么意思。高、中、低级IF的比较优势和劣势是什么?

6. Explain what is meant by the “level” of an intermediate form (IF). What are the comparative advantages and disadvantages of high-, medium-, and low-level IFs?

7.  Ada 编译器中最常用的 IF 是什么?

7. What is the IF most commonly used in Ada compilers?

8. 说出基于堆栈的 IF 的两个优点。说出一个缺点。

8. Name two advantages of a stack-based IF. Name one disadvantage.

9. 解释基于单个 IF 构建一系列编译器(几种语言、几种目标机器)的理由。

9. Explain the rationale for basing a family of compilers (several languages, several target machines) on a single IF.

10. 为什么编译器可能使用多个IF?

10. Why might a compiler employ more than one IF?

11. 概述后端编译器组织和结构的一些主要设计方案。

11. Outline some of the major design alternatives for back-end compiler organization and structure.

12. 有时什么被称为编译器的“中端”?

12. What is sometimes called the “middle end” of a compiler?

13. 为什么对有限的物理寄存器集的管理通常被推迟到编译过程的后期?

13. Why is management of a limited set of physical registers usually deferred until late in the compilation process?

15.4 地址空间组织

15.4 Address Space Organization

汇编器、链接器和加载器通常对一对相关文件格式进行操作:可重定位目标代码和可执行目标代码。可重定位目标代码可作为链接器的输入;可以组合此格式的多个文件以创建可执行程序。可执行目标代码可作为加载器的输入:可将其载入内存并运行。可重定位目标文件包括以下描述性信息:

Assemblers, linkers, and loaders typically operate on a pair of related file formats: relocatable object code and executable object code. Relocatable object code is acceptable as input to a linker; multiple files in this format can be combined to create an executable program. Executable object code is acceptable as input to a loader: it can be brought into memory and run. A relocatable object file includes the following descriptive information:

导入表:标识指向地址未知的命名位置的指令,但推测位于尚未链接到此文件的其他文件中。

Import table: Identifies instructions that refer to named locations whose addresses are unknown, but are presumed to lie in other files yet to be linked to this one.

重定位表:标识引用当前文件内位置的指令,但必须在链接时进行修改以反映当前文件在最终可执行程序中的偏移量。

Relocation table: Identifies instructions that refer to locations within the current file, but that must be modified at link time to reflect the offset of the current file within the final, executable program.

导出表:列出当前文件中可能被其他文件引用的位置的名称和地址。

Export table: Lists the names and addresses of locations in the current file that maybe referred to in other files.

导入和导出的名称称为外部符号

Imported and exported names are known as external symbols.

可执行目标文件的特点是它不包含对外部符号的引用(至少如果是静态链接的话——下面会详细介绍)。它还定义了执行的起始地址。可执行文件可能是或不是可重定位的,这取决于它是否包含上述表格。

An executable object file is distinguished by the fact that it contains no references to external symbols (at least if statically linked—more on this below). It also defines a starting address for execution. An executable file may or may not be relocatable, depending on whether it contains the tables above.

目标文件结构的细节因操作系统而异。但通常,目标文件分为几个部分,每个部分由链接器、加载器或操作系统以不同的方式处理。第一部分包括导入、导出和重定位表,以及对程序需要多少空间来存储未初始化的静态数据的指示。其他部分通常包括代码(指令)、只读数据(常量、case语句的跳转表等)、已初始化但可写的静态数据以及编译器保存的符号表和布局信息。初始描述部分由链接器和加载器使用。符号表部分由调试器和性能分析器使用(第16.3.216.3.3节)。这两个表通常都不会在运行时载入内存;大多数正在运行的程序都不需要它们(如果程序使用反射机制 [第 16.3.1 节] 来检查其自身的类型结构,则会出现例外情况)。

Details of object file structure vary from one operating system to another. Typically, however, an object file is divided into several sections, each of which is handled differently by the linker, loader, or operating system. The first section includes the import, export, and relocation tables, together with an indication of how much space will be required by the program for noninitialized static data. Other sections commonly include code (instructions), read-only data (constants, jump tables for case statements, etc.), initialized but writable static data, and symbol table and layout information saved by the compiler. The initial descriptive section is used by the linker and loader. The symbol table section is used by debuggers and performance profilers (Sections 16.3.2 and 16.3.3). Neither of these tables is usually brought into memory at run time; neither is needed by most running programs (an exception occurs in the case of programs that employ reflection mechanisms [Section 16.3.1] to examine their own type structure).

在可运行(加载)形式中,程序通常组织为若干。在某些机器(例如 80286 或 PA-RISC)上,段对于汇编语言程序员来说是可见的,并且可以在指令中明确命名。在现代机器上,段更常见于操作系统以不同方式管理的地址空间子集。其中某些段(特别是代码、常量和初始化数据)对应于目标文件的各个部分。代码和常量通常是只读的,并且常组合在一个段中;如果程序试图修改它们,操作系统会安排接收中断。(作为对此类中断的响应,操作系统很可能会打印一条错误消息并终止程序。)初始化数据是可写的。在加载时,操作系统从磁盘读取代码、常量和初始化数据,或者安排在运行时读入它们,以响应“无效访问”(页面错误)中断或动态链接请求。

In its runnable (loaded) form, a program is typically organized into several segments. On some machines (e.g., the 80286 or PA-RISC), segments were visible to the assembly language programmer, and could be named explicitly in instructions. More commonly on modern machines, segments are simply subsets of the address space that the operating system manages in different ways. Some of them—code, constants, and initialized data in particular—correspond to sections of the object file. Code and constants are usually read-only, and are often combined in a single segment; the operating system arranges to receive an interrupt if the program attempts to modify them. (In response to such an interrupt it will most likely print an error message and terminate the program.) Initialized data are writable. At load time, the operating system either reads code, constants, and initialized data from disk, or arranges to read them in at run time, in response to “invalid access” (page fault) interrupts or dynamic linking requests.

除了代码、常量和初始化数据之外,典型的运行程序还有几个额外的段:

In addition to code, constants, and initialized data, the typical running program has several additional segments:

未初始化数据:可能在加载时分配,或根据需要分配以响应页面错误。通常用零填充,既可以为错误读取尚未写入的数据的程序提供可重复的症状,也可以通过阻止程序读取先前用户写入的页面内容来增强多用户系统的安全性。

Uninitialized data: May be allocated at load time or on demand in response to page faults. Usually zero-filled, both to provide repeatable symptoms for programs that erroneously read data they have not yet written, and to enhance security on multiuser systems, by preventing a program from reading the contents of pages written by previous users.

堆: 可以在加载时分配固定大小。更常见的是,初始大小较小,然后操作系统会自动扩展以响应超出当前段末尾的(故障)访问。

Stack: May be allocated in some fixed amount at load time. More commonly, is given a small initial size, and is then extended automatically by the operating system in response to (faulting) accesses beyond the current segment end.

堆:与堆栈类似,在加载时可能会分配一定数量的空间。更常见的是,堆的初始大小较小,然后根据堆管理库例程的明确请求(通过系统调用)进行扩展。

Heap: Like stack, may be allocated in some fixed amount at load time. More commonly, is given a small initial size, and is then extended in response to explicit requests (via system call) from heap-management library routines.

文件:在许多系统中,库例程允许程序将文件映射到内存中。映射例程与操作系统交互,为文件创建新段,并返回该段开头的地址。段的内容通常是根据需要从磁盘中提取的,以响应页面错误。

Files: In many systems, library routines allow a program to map a file into memory. The map routine interacts with the operating system to create a new segment for the file, and returns the address of the beginning of the segment. The contents of the segment are usually fetched from disk on demand, in response to page faults.

动态库:现代操作系统通常安排大多数程序共享常用库的单一代码副本(第 C-15.7 节)。从单个进程的角度来看,每个此类库往往占用一对段:一个用于共享代码,一个用于链接信息,一个用于库可能需要的任何可写数据的私有副本。

Dynamic libraries: Modern operating systems typically arrange for most programs to share a single copy of the code for popular libraries (Section C-15.7). From the point of view of an individual process, each such library tends to occupy a pair of segments: one for the shared code, one for linkage information and for a private copy of any writable data the library may need.

例 15.9

Example 15.9

Linux 地址空间布局

Linux address space layout

图 15.8显示了当前 x86 上的 32 位 Linux 系统中这些段的布局。其他操作系统和机器的相对位置和地址可能有所不同。■

The layout of these segments for a contemporary 32-bit Linux system on the x86 appears in Figure 15.8. Relative placements and addresses may be different for other operating systems and machines. ■

f15-09-9780124104099
图 15.8 x86 Linux 中 32 位进程地址空间的布局(未按比例)。双线分隔具有潜在不同访问权限的区域。

15.5 组装

15.5 Assembly

有些编译器会直接将源文件翻译成链接器可以接受的目标文件。更常见的是,它们会生成汇编语言,然后由汇编器处理该语言以创建目标文件。

Some compilers translate source files directly into object files acceptable to the linker. More commonly, they generate assembly language that must subsequently be processed by an assembler to create an object file.

在我们的示例中,我们始终使用符号(文本)表示法来表示代码。在编译器中,表示形式不是文本,但仍是符号,很可能由记录和链接列表组成。要将此符号表示转换为可执行代码,我们必须

In our examples we have consistently employed a symbolic (textual) notation for code. Within a compiler, the representation would not be textual, but it would still be symbolic, most likely consisting of records and linked lists. To translate this symbolic representation into executable code, we must

1. 用机器语言编码替换操作码和操作数。

1. Replace opcodes and operands with their machine language encodings.

2. 用实际地址替换符号名称的使用。

2. Replace uses of symbolic names with actual addresses.

这些是装配工的主要任务。

These are the principal tasks of an assembler.

在计算机发展的早期,大多数程序员都使用汇编语言编写程序。为了简化汇编编程中繁琐而重复的部分,汇编程序通常提供大量宏扩展功能。随着现代高级语言的普及,这种以程序员为中心的特性已基本消失。如今,几乎所有的汇编语言程序都是由编译器编写的。

In the early days of computing, most programmers wrote in assembly language. To simplify the more tedious and repetitive aspects of assembly programming, assemblers often provided extensive macro expansion facilities. With the ubiquity of modern high-level languages, such programmer-centric features have largely disappeared. Almost all assembly language programs today are written by compilers.

例 15.10

Example 15.10

汇编作为最终的编译过程

Assembly as a final compiler pass

当将汇编语言直接从编译器传递到汇编器时,使用一些内部(记录和链接列表)表示是有意义的。同时,我们必须提供一个文本前端来适应偶尔的人工输入的需要:

When passing assembly language directly from the compiler to the assembler, it makes sense to use some internal (records and linked lists) representation. At the same time, we must provide a textual front end to accommodate the occasional need for human input:

u15-01-9780124104099

汇编器前端只是将文本源转换为内部符号形式。通过共享汇编器后端,编译器和汇编器前端可避免重复工作。出于调试目的,编译器通常会选择转储传递给汇编器的代码的文本表示形式。■

The assembler front end simply translates textual source into internal symbolic form. By sharing the assembler back end, the compiler and assembler front end avoid duplication of effort. For debugging purposes, the compiler will generally have an option to dump a textual representation of the code it passes to the assembler. ■

例 15.11

Example 15.11

直接生成目标代码

Direct generation of object code

另一种组织方式是让编译器直接生成目标代码:

An alternative organization has the compiler generate object code directly:

u15-02-9780124104099

这种组织方式为编译器提供了更多的灵活性:如果需要,可以更早地执行通常由汇编程序执行的操作(例如,将地址分配给变量)。由于没有单独的汇编过程,因此整体转换为目标代码的速度可能会稍快一些。独立汇编程序可以相对简单。如果它仅用于小型专用代码片段,则可能不需要执行指令调度或其他特定于机器的代码改进。使用反汇编程序而不是编译器的汇编语言转储可确保程序员看到的内容与目标文件中的内容完全对应。如果编译器使用更高级的汇编程序作为后端,则汇编程序对程序所做的任何修改都不会在编译器转储的汇编语言中可见。■

This organization gives the compiler a bit more flexibility: operations normally performed by an assembler (e.g., assignment of addresses to variables) can be performed earlier if desired. Because there is no separate assembly pass, the overall translation to object code may be slightly faster. The stand-alone assembler can be relatively simple. If it is used only for small, special-purpose code fragments, it probably doesn't need to perform instruction scheduling or other machine-specific code improvement. Using a disassembler instead of an assembly language dump from the compiler ensures that what the programmer sees corresponds precisely to what is in the object file. If the compiler uses a fancier assembler as a back end, then any program modifications effected by the assembler will not be visible in the assembly language dumped by the compiler. ■

15.5.1 发射指令

15.5.1 Emitting Instructions

例 15.12

Example 15.12

压缩nops

Compressing nops

汇编器的最基本任务是将指令的符号表示转换为二进制形式。在某些汇编器中,这是一项非常简单的任务,因为助记符操作和指令操作码之间存在一一对应关系。但是,许多汇编器对其输入进行细微更改,以提高性能或扩展指令集,使汇编语言更易于人类阅读。GNU 汇编器gas是比较保守的汇编器之一,但即使它也要采取一些自由。例如,某些编译器会生成 nop 指令来缓存对齐某些基本块(例如函数序言)。为了减少这些指令所消耗的周期数,gas会将多个连续的 nop 组合成无影响的多字节指令。(在 x86 上, lea指令有 2 字节、4 字节和 7 字节变体,可用于将寄存器移动到自身。)■

The most basic task of the assembler is to translate symbolic representations of instructions into binary form. In some assemblers this is an entirely straightforward task, because there is a one-to-one correspondence between mnemonic operations and instruction op-codes. Many assemblers, however, make minor changes to their input in order to improve performance or to extend the instruction set in ways that make the assembly language easier for human beings to read. The GNU assembler, gas, is among the more conservative, but even it takes a few liberties. For example, some compilers generate nop instructions to cache-align certain basic blocks (e.g., function prologues). To reduce the number of cycles these consume, gas will combine multiple consecutive nops into multibyte instructions that have no effect. (On the x86, there are 2-, 4-, and 7-byte variants of the lea instruction that can be used to move a register into itself.) ■

例 15.13

Example 15.13

相对分支和绝对分支

Relative and absolute branches

对于跳转到附近的地址,gas 使用指定与pc偏移量的指令变体。对于跳转到较远的地址(或直到链接时才知道的地址),它使用指定绝对地址的较长变体。一些 x86 指令(通常不是由现代编译器生成的)没有较长的变体。对于这些,一些汇编程序将反转条件测试的意义以跳过无条件跳转。Gas根本无法处理它们。

For jumps to nearby addresses, gas uses an instruction variant that specifies an offset from the pc. For jumps to distant addresses (or to addresses not known until link time), it uses a longer variant that specifies an absolute address. A few x86 instructions (not typically generated by modern compilers) don't have the longer variant. For these, some assemblers will reverse the sense of the conditional test to hop over an unconditional jump. Gas simply fails to handle them. ■

例 15.14

Example 15.14

伪指令

Pseudoinstructions

在更激进的方面,SGI 的 MIPS 指令集汇编器提供了大量的伪指令,这些伪指令根据其参数转换为不同的实指令,或对应于多指令序列。例如,MIPS 上有两条整数加法指令:其中一条加两个寄存器;另一条加一个寄存器和一个常数。汇编器提供一条伪指令,并将其转换为适当的变体。类似地,汇编器提供一条伪指令来将任意常数加载到寄存器中。由于所有指令都是 32 位长,因此当常数无法容纳在 16 位中时,必须将此伪指令转换为一对实指令。一些伪指令可能会生成更长的序列。整数除法可能需要多达 11 条实指令,以检查错误并将商从临时位置移动到所需的寄存器。■

At the more aggressive end of the spectrum, SGI's assembler for the MIPS instruction set provides a large number of pseudoinstructions that translate into different real instructions depending on their arguments, or that correspond to multi-instruction sequences. For example, there are two integer add instructions on the MIPS: one of them adds two registers; the other adds a register and a constant. The assembler provides a single pseudoinstruction, which it translates into the appropriate variant. In a similar vein, the assembler provides a pseudoinstruction to load an arbitrary constant into a register. Since all instructions are 32 bits long, this pseudoinstruction must be translated into a pair of real instructions when the constant won't fit in 16 bits. Some pseudoinstructions may generate even longer sequences. Integer division can take as many as 11 real instructions, to check for errors and to move the quotient from a temporary location into the desired register. ■

实际上,SGI 汇编器实现了真实机器的“清理”版本。除了提供伪指令外,它还重新组织指令以隐藏延迟分支的存在(第 C-5.5.1 节)并提高处理器流水线的预期性能。此重组构成指令调度的最后一道关口(第 C-5.5.1 节和 C-17.6 节)。虽然这项工作可以由编译器处理,但整数除法示例等伪指令的存在强烈支持在汇编器中执行此操作。除了具有可能由相邻指令填充的两个分支延迟之外,扩展的除法序列还可以用作指令源来填充附近的分支、加载或功能单元延迟。

In effect, the SGI assembler implements a “cleaned-up” variant of the real machine. In addition to providing pseudoinstructions, it reorganizes instructions to hide the existence of delayed branches (Section C-5.5.1) and to improve the expected performance of the processor pipeline. This reorganization constitutes a final pass of instruction scheduling (Sections C-5.5.1 and C-17.6). Though the job could be handled by the compiler, the existence of pseudoinstructions like the integer division example argues strongly for doing it in the assembler. In addition to having two branch delays that might be filled by neighboring instructions, the expanded division sequence can be used as a source of instructions to fill nearby branch, load, or functional unit delays.

例 15.15

Example 15.15

汇编程序指令

Assembler directives

除了将符号指令表示转换为二进制指令表示之外,大多数汇编程序还响应各种指令。Gas提供了 100 多个这样的指令。下面是几个示例

In addition to translating from symbolic to binary instruction representations, most assemblers respond to a variety of directives. Gas provides more than 100 of these. A few examples follow.

段切换: .text指令表示后续指令和数据应放置在代码(文本)段中。.data指令表示后续指令和数据应放置在已初始化的数据段中。 虽然不常见,但可以将指令放置在数据段中,或将数据放置在代码段中。).space n指令表示应在未初始化的数据段中保留n个字节的空间。(后一个指令通常以标签开头。)

Segment switching: The .text directive indicates that subsequent instructions and data should be placed in the code (text) segment. The .data directive indicates that subsequent instructions and data should be placed in the initialized data segment. (It is possible, though uncommon, to put instructions in the data segment, or data in the code segment.) The .space n directive indicates that n bytes of space should be reserved in the uninitialized data segment. (This latter directive is usually preceded by a label.)

数据生成: .byte 、.hword、.word、.float.double指令均采用一系列参数,这些参数放置在输出程序当前段的连续位置中。它们的不同之处在于操作数的类型。相关的.ascii指令采用单个字符串作为参数,这些参数放置在连续的字节中。

Data generation: The .byte, .hword, .word, .float, and .double directives each take a sequence of arguments, which they place in successive locations in the current segment of the output program. They differ in the types of operands. The related .ascii directive takes a single character string as argument, which it places in consecutive bytes.

符号标识:.globl name指令表示该名称应该输入到导出符号表中。

Symbol identification: The .globl name directive indicates that name should be entered into the table of exported symbols.

对齐:.align n指令导致后续输出与能被 2 n整除的地址对齐。

Alignment: The .align n directive causes the subsequent output to be aligned at an address evenly divisible by 2n.

15.5.2 将地址分配给名称

15.5.2 Assigning Addresses to Names

与编译器一样,汇编器通常分为几个阶段。如果输入是文本,则初始阶段将扫描和解析输入,并构建内部表示。在最常见的组织中,还有两个附加阶段。第一个阶段识别所有内部和外部(导入)符号,将位置分配给内部符号。这个阶段很复杂,因为某些指令的长度(在 CISC 机器上)或伪指令生成的实际指令的数量(在 RISC 机器上)可能取决于地址中的有效位数。给定符号值,最后阶段生成目标代码。

Like compilers, assemblers commonly work in several phases. If the input is textual, an initial phase scans and parses the input, and builds an internal representation. In the most common organization there are two additional phases. The first identifies all internal and external (imported) symbols, assigning locations to the internal ones. This phase is complicated by the fact that the length of some instructions (on a CISC machine) or the number of real instructions produced by a pseudoinstruction (on a RISC machine) may depend on the number of significant bits in an address. Given values for symbols, the final phase produces object code.

在目标文件中,.globl指令中提到的任何符号都必须出现在导出符号表中,并带有一个指示符号地址的条目。指令或指令中引用的任何符号,但未在输入程序中定义,都必须出现在导入符号表中,并带有一个条目,用于标识代码中出现此类引用的所有位置。最后,任何指令或数据(其值取决于当前文件在正在运行的程序的地址空间中的位置)都必须列在重定位表中。

Within the object file, any symbol mentioned in a .globl directive must appear in the table of exported symbols, with an entry that indicates the symbol's address. Any symbol referred to in a directive or an instruction, but not defined in the input program, must appear in the table of imported symbols, with an entry that identifies all places in the code at which such references occur. Finally, any instruction or datum whose value depends on the placement of the current file within the address space of a running program must be listed in the relocation table.

例 15.16

Example 15.16

目标文件中地址的编码

Encoding of addresses in object files

从历史上看,汇编器会区分目标文件中的绝对字和可重定位字。绝对字在汇编时已知;它们不需要由链接器更改。示例包括常量和寄存器-寄存器指令。相反,可重定位字需要通过向其添加它所在的目标文件的代码或数据段的最终程序中的地址来进行修改。例如,CISC 跳转指令可能由 1 字节的jmp操作码和后跟 4 字节的目标地址组成。对于本地目标,目标文件中的地址字节将包含符号在文件中的偏移量。链接器将通过在程序的最终版本中添加文件代码段的地址来确定地址。

Historically, assemblers distinguished between absolute and relocatable words in an object file. Absolute words were known at assembly time; they did not need to be changed by the linker. Examples include constants and register–register instructions. A relocatable word, in contrast, needed to be modified by adding to it the address within the final program of the code or data segment of the object file in which it appeared. A CISC jump instruction, for example, might consist of a 1-byte jmp opcode followed by a 4-byte target address. For a local target, the address bytes in the object file would contain the symbol's offset within the file. The linker would finalize the address by adding the address of the file's code segment in the final version of the program.

在现代机器上,这种单一形式的重定位已不再足够。地址以多种不同的方式编码到指令中,并且这些编码必须反映在重定位表和导入表中。例如,在 32 位 ARM 处理器上,无条件分支 ( b ) 指令具有 24 位偏移字段。处理器将此字段左移两位,对其进行符号扩展,然后将其添加到分支指令本身的地址以获取目标地址。1

On modern machines, this single form of relocation no longer suffices. Addresses are encoded into instructions in many different ways, and these encodings must be reflected in the relocation table and the import table. On a 32-bit ARM processor, for example, an unconditional branch (b) instruction has a 24-bit offset field. The processor left-shifts this field by two bits, sign-extends it, and then adds it to the address of the branch instruction itself to obtain the target address.1

要重定位这样的指令,链接器必须将目标代码段的地址和目标指令在其中的偏移量相加,减去当前代码段的地址和分支指令在其中的偏移量,执行两位右移算术移位,并将结果截断为 24 位。类似地,ARM 上的 32 位加载需要类似于示例 15.14的两条指令序列;如果加载的数量是可重定位的,链接器必须重新计算两条指令的 16 位操作数。现代汇编程序和目标文件格式反映了这种重定位模式的多样性。■

To relocate such an instruction, the linker must add the address of the target code segment and the offset within it of the target instruction, subtract the address of the current code segment and the offset within it of the branch instruction, perform a two-bit right arithmetic shift, and truncate the result to 24 bits. In a similar vein, a 32-bit load on ARM requires a two-instruction sequence analogous to that of Example 15.14; if the loaded quantity is relocatable, the linker must recalculate the 16-bit operands of both instructions. Modern assemblers and object file formats reflect this diversity of relocation modes. ■

15.6 链接

15.6 Linking

大多数语言实现(当然,所有用于构建大型程序的语言实现)都支持单独编译:程序的各个片段可以或多或少独立地进行编译和组装。编译后,这些片段(称为编译单元)由链接器“粘合在一起” 。在许多语言和环境中,程序员明确将程序划分为模块或文件,每个模块或文件都单独编译。集成程度更高的环境可能会放弃文件的概念,转而采用子例程数据库,每个子例程都单独编译。

Most language implementations—certainly all that are intended for the construction of large programs—support separate compilation: fragments of the program can be compiled and assembled more or less independently. After compilation, these fragments (known as compilation units) are “glued together” by a linker. In many languages and environments, the programmer explicitly divides the program into modules or files, each of which is separately compiled. More integrated environments may abandon the notion of a file in favor of a database of subroutines, each of which is separately compiled.

链接器的任务是将编译单元连接在一起。静态链接器在程序执行之前完成其工作,生成可执行目标文件。动态链接器(如第 C-15.7 节所述)在程序(第一部分)进入内存执行后完成其工作。

The task of a linker is to join together compilation units. A static linker does its work prior to program execution, producing an executable object file. A dynamic linker (described in Section C-15.7) does its work after the (first part of the) program has been brought into memory for execution.

每个要链接的编译单元都必须是可重定位的目标文件。通常,一些文件是通过编译正在构建的应用程序的片段生成的,而其他文件则是应用程序所需的预先存在的库包。由于大多数程序都使用库,因此即使是“单文件”应用程序通常也需要链接。

Each to-be-linked compilation unit must be a relocatable object file. Typically, some files will have been produced by compiling fragments of the application being constructed, while others will be preexisting library packages needed by the application. Since most programs make use of libraries, even a “one-file” application typically needs to be linked.

链接涉及两个子任务:重定位和外部引用的解析。有些作者将重定位称为加载,并将整个“连接在一起”的过程称为“链接加载”。其他作者(包括当前作者)使用“加载”来指将可执行目标文件放入内存中执行的过程。在非常简单的机器上,或者在操作系统非常简单的机器上,加载需要重定位。更常见的是,操作系统使用虚拟内存让每个程序都觉得它从某个标准地址启动。在许多系统中,加载还需要一定量的链接(第 C-15.7 节)。

Linking involves two subtasks: relocation and the resolution of external references. Some authors refer to relocation as loading, and call the entire “joining together” process “link-loading.” Other authors (including the current one) use “loading” to refer to the process of bringing an executable object file into memory for execution. On very simple machines, or on machines with very simple operating systems, loading entails relocation. More commonly, the operating system uses virtual memory to give every program the impression that it starts at some standard address. In many systems loading also entails a certain amount of linking (Section C-15.7).

15.6.1 重定位和名称解析

15.6.1 Relocation and Name Resolution

例 15.17

Example 15.17

静态链接

Static linking

每个可重定位目标文件都包含链接所需的信息:导入、导出和重定位表。静态链接器在一个两阶段过程中使用此信息,该过程类似于第 15.5 节中描述的汇编器过程。在第一阶段,链接器将所有编译单元聚集在一起,在内存中为它们选择顺序,并记下每个单元的最终地址。在第二阶段,链接器处理每个单元,用适当的地址替换未解析的外部引用,并修改需要重定位的指令以反映其单元的地址。图 15.9以图形方式说明了这些阶段。假设地址和偏移量以十六进制表示法书写,页面大小为 4K(1000 16)字节。■

Each relocatable object file contains the information required for linking: the import, export, and relocation tables. A static linker uses this information in a two-phase process analogous to that described for assemblers in Section 15.5. In the first phase, the linker gathers all of the compilation units together, chooses an order for them in memory, and notes the address at which each will consequently lie. In the second phase, the linker processes each unit, replacing unresolved external references with appropriate addresses, and modifying instructions that need to be relocated to reflect the addresses of their units. These phases are illustrated pictorially in Figure 15.9. Addresses and offsets are assumed to be written in hexadecimal notation, with a page size of 4K (100016) bytes. ■

f15-10-9780124104099
图 15.9 将可重定位目标文件 A 和 B 链接起来以生成可执行目标文件。为便于展示,A 的代码部分位于偏移量 0 处,B 的代码部分紧随其后;偏移量 800(地址沿页面向下增加)。为了允许操作系统为代码段和数据段建立不同的保护,A 的数据部分位于下一页边界(偏移量 3000),B 的数据部分紧随其后(偏移量 3500)。对 M 和 X 的外部引用已设置为使用适当的地址。对 L 和 Y 的内部引用已通过分别添加 B 的代码和数据部分的起始地址进行了更新

库是一个挑战。许多库由数百个单独编译的程序片段组成,其中大多数程序片段对于任何特定的应用程序来说都不是必需的。应用程序。链接器不需要将整个库链接到每个应用程序中,而是需要搜索库以识别从主程序引用的片段。如果这些片段引用了其他片段,则还必须以递归方式包含这些片段。许多系统支持可重定位目标文件的特殊库格式。这种格式的库可以包含任意数量的代码和数据部分,以及将符号名称映射到它们出现的部分的索引。

Libraries present a bit of a challenge. Many consist of hundreds of separately compiled program fragments, most of which will not be needed by any particular application. Rather than link the entire library into every application, the linker needs to search the library to identify the fragments that are referenced from the main program. If these refer to additional fragments, then those must be included also, recursively. Many systems support a special library format for relocatable object files. A library in this format may contain an arbitrary number of code and data sections, together with an index that maps symbol names to the sections in which they appear.

15.6.2 类型检查

15.6.2 Type Checking

在编译单元内,编译器强制执行静态语义规则。在单元之间,它使用模块头文件来强制执行与外部引用有关的规则。实际上,模块M的头文件就M与其用户的接口做出了一系列承诺。在编译M的主体时,编译器确保这些承诺得以兑现。但是,想象一下,如果我们编译了M的主体,然后在编译某个用户模块U之前更改了其头文件中某些子例程的参数数量和类型,会发生什么情况。如果两次编译都成功,那么MU对如何解释在它们之间传递的参数的概念将非常不同;虽然它们可能仍然链接在一起,但在运行时可能会出现混乱。为了防止出现这种问题,我们必须确保每当MU链接在一起时,它们都是使用相同版本的M头文件进行编译的。

Within a compilation unit, the compiler enforces static semantic rules. Across the boundaries between units, it uses module headers to enforce the rules pertaining to external references. In effect, the header for module M makes a set of promises regarding M's interface to its users. When compiling the body of M, the compiler ensures that those promises are kept. Imagine what could happen, however, if we compiled the body of M, and then changed the numbers and types of parameters for some of the subroutines in its header file before compiling some user module U. If both compilations succeed, then M and U will have very different notions of how to interpret the parameters passed between them; while they may still link together, chaos is likely to ensue at run time. To prevent this sort of problem, we must ensure whenever M and U are linked together that both were compiled using the same version of M's header.

在大多数基于模块的语言中,以下技术就足够了。在编译模块M的主体时,我们创建一个虚拟符号,其名称唯一地表征了M的标头的内容。在编译U的主体时,我们创建对虚拟符号的引用。只有当 M 和 U 在符号名称上一致时,将MU链接在一起的尝试才会成功。

In most module-based languages, the following technique suffices. When compiling the body of module M we create a dummy symbol whose name uniquely characterizes the contents of M's header. When compiling the body of U we create a reference to the dummy symbol. An attempt to link M and U together will succeed only if they agree on the name of the symbol.

例 15.18

Example 15.18

对标头进行校验以确保一致性

Checksumming headers for consistency

创建表征M 的符号名称的一种方法是使用M的标头最近修改时间的文本表示。然而,由于文件可能会在机器之间移动(例如,将源文件交付给地理上分散的客户),修改时间是有问题的:不同机器上的时钟可能同步性较差,文件复制操作经常更改修改时间。更好的候选方法是头文件的校验和:本质上是使用文件整个文本作为密钥的哈希函数的输出。理论上,两个不同但有效的文件可能具有相同的校验和,但如果选择良好的哈希函数,出现此错误的几率将非常小。■

One way to create the symbol name that characterizes M is to use a textual representation of the time of the most recent modification of M's header. Because files may be moved across machines, however (e.g., to deliver source files to geographically distributed customers), modification times are problematic: clocks on different machines may be poorly synchronized, and file copy operations often change the modification time. A better candidate is a checksum of the header file: essentially the output of a hash function that uses the entire text of the file as key. It is possible in theory for two different but valid files to have the same checksum, but with a good choice of hash function the odds of this error are exceedingly small.■

设计与实现

Design & Implementation

15.2 单独编译的类型检查

15.2 Type checking for separate compilation

在 C++ 中,符号名称中的类型信息编码效果很好,但在 C 中使用则过于严格:这会禁止使用语言定义允许的、虽然值得怀疑的编程技巧。在 C++ 中,通过使用类型的结构等效性来简化符号名称编码。原则上,可以在具有名称等效性的语言中使用它,但考虑到此类语言通常具有结构良好的模块,使用标头的校验和更为简单。

The encoding of type information in symbol names works well in C++, but is too strict for use in C: it would outlaw programming tricks that, while questionable, are permitted by the language definition. Symbol-name encoding is facilitated in C++ by the use of structural equivalence for types. In principle, one could use it in a language with name equivalence, but given that such languages generally have well-structured modules, it is simpler just to use a checksum of the header.

校验和策略确实要求我们知道何时使用模块头。不幸的是,如第 C-3.8 节所述,我们在 C 和 C++ 中不知道这一点:这些语言中的头文件只是一种编程约定,由语言预处理器的文本包含机制支持。大多数 C 实现并不在链接时强制接口的一致性;相反,程序员依靠配置管理工具(例如 Unix 的make)在必要时重新编译文件。此类工具通常由文件修改时间驱动。

The checksum strategy does require that we know when we're using a module header. Unfortunately, as described in Section C-3.8, we don't know this in C and C++: headers in these languages are simply a programming convention, supported by the textual inclusion mechanism of the language's preprocessor. Most implementations of C do not enforce consistency of interfaces at link time; instead, programmers rely on configuration management tools (e.g., Unix's make) to recompile files when necessary. Such tools are typically driven by file modification times.

C++ 的大多数实现采用了一种不同的方法,有时称为名称改编。目标文件中每个导入或导出符号的名称都是通过将程序源中的相应名称与其类型的表示连接起来而创建的。对于对象,类型由类名和其结构的简洁编码组成。对于函数,它由参数类型和返回值的编码组成。对于具有许多参数的复杂对象或函数,生成的名称可能会非常长。如果链接器将符号限制为某个过小的最大长度,则可以通过散列来压缩类型信息,但安全性会略有损失 [ SF88 ]。

Most implementations of C++ adopt a different approach, sometimes called name mangling. The name of each imported or exported symbol in an object file is created by concatenating the corresponding name from the program source with a representation of its type. For an object, the type consists of the class name and a terse encoding of its structure. For a function, it consists of an encoding of the types of the arguments and the return value. For complicated objects or functions of many arguments, the resulting names can be very long. If the linker limits symbols to some too-small maximum length, the type information can be compressed by hashing, at some small loss in security [SF88].

任何基于文件修改时间或校验和的技术都存在一个问题,即对头文件的微小更改(例如,修改注释或定义现有接口用户不需要的新常量)都可能导致文件无法正确链接。配置管理工具也存在类似的问题:微小的更改可能会导致工具不必要地重新编译文件。一些编程环境通过以小于编译单元的粒度跟踪更改来解决此问题 [ Tic86 ]。大多数只是需要重新编译。

One problem with any technique based on file modification times or checksums is that a trivial change to a header file (e.g., modification of a comment, or definition of a new constant not needed by existing users of the interface) can prevent files from linking correctly. A similar problem occurs with configuration management tools: a trivial change may cause the tool to recompile files unnecessarily. A few programming environments address this issue by tracking changes at a granularity smaller than the compilation unit [Tic86]. Most just live with the need to recompile.

15.7 动态链接

15.7 Dynamic Linking

在多用户系统中,一个程序(例如编辑器或 Web 浏览器)的多个实例同时执行是很常见的。为每个正在运行的程序实例分配内存空间用于单独的相同代码副本是非常浪费的。因此,许多操作系统会跟踪正在运行的程序,并设置内存映射表,以便同一程序的所有实例共享该程序代码段的同一份只读副本。每个实例都会收到其自己的可写数据段副本。代码段共享可以节省大量空间。但是,对于相似但不完全相同的程序实例,它不起作用。

On a multiuser system, it is common for several instances of a program (e.g., an editor or web browser) to be executing simultaneously. It would be highly wasteful to allocate space in memory for a separate, identical copy of the code of such a program for every running instance. Many operating systems therefore keep track of the programs that are running, and set up memory mapping tables so that all instances of the same program share the same read-only copy of the program's code segment. Each instance receives its own writable copy of the data segment. Code segment sharing can save enormous amounts of space. It does not work, however, for instances of programs that are similar but not identical.

许多程序集虽然不完全相同,但都具有大量共同的库代码——例如用于管理图形用户界面。如果每个应用程序如果有自己的库副本,则可能会浪费大量内存。此外,如果程序是静态链接的,则在单独的可执行目标文件中几乎相同的库副本上可能会浪费大量磁盘空间。

Many sets of programs, while not identical, have large amounts of library code in common—for example to manage a graphical user interface. If every application has its own copy of the library, then large amounts of memory may be wasted. Moreover, if programs are statically linked, then much larger amounts of disk space may be wasted on nearly identical copies of the library in separate executable object files.

15-02-9780124104099 更深入地

IN MORE DEPTH

在 20 世纪 90 年代初期,大多数操作系统供应商都采用了动态链接,以节省内存和磁盘空间。我们在配套网站上更详细地讨论了此选项。每个动态链接库都驻留在其自己的代码和数据段中。使用给定库的每个程序实例都拥有库数据段的私有副本,但共享库代码段的单个系统范围的只读副本。这些段可能在程序加载到内存时链接到代码的其余部分,或者在执行期间根据需要逐步链接。除了节省空间之外,动态链接还允许程序员或系统管理员安装库的向后兼容更新,而无需重建所有现有的可执行目标文件:下次运行时,每个程序都会自动获取库的新版本。

In the early 1990s, most operating system vendors adopted dynamic linking in order to save space in memory and on disk. We consider this option in more detail on the companion site. Each dynamically linked library resides in its own code and data segments. Every program instance that uses a given library has a private copy of the library's data segment, but shares a single system-wide read-only copy of the library's code segment. These segments maybe linked to the remainder of the code when the program is loaded into memory, or they may be linked incrementally on demand, during execution. In addition to saving space, dynamic linking allows a programmer or system administrator to install backward-compatible updates to a library without rebuilding all existing executable object files: the next time it runs, each program will obtain the new version of the library automatically.

15-01-9780124104099检查你的理解

Check Your Understanding

14. 可重定位目标文件和可执行目标文件有哪些显著特征?

14. What are the distinguishing characteristics of a relocatable object file? An executable object file?

15. 为什么操作系统通常用零填充未初始化数据的页面?

15. Why do operating systems typically zero-fill pages used for uninitialized data?

16.列出 装配工通常执行的四项任务。

16. List four tasks commonly performed by an assembler.

17. 总结汇编语言和目标代码作为编译器输出的比较优势。

17. Summarize the comparative advantages of assembly language and object code as the output of a compiler.

18.给出 汇编程序可能提供的三个伪指令示例和三个指令示例。

18. Give three examples of pseudoinstructions and three examples of directives that an assembler might be likely to provide.

19. 为什么汇编程序可以执行其自己的最终指令调度?

19. Why might an assembler perform its own final pass of instruction scheduling?

20.解释目标文件中 绝对字和可重定位字之间的区别。为什么“可重定位性”的概念比以前更复杂了?

20. Explain the distinction between absolute and relocatable words in an object file. Why is the notion of “relocatability” more complicated than it used to be?

21. 链接加载有什么区别?

21. What is the difference between linking and loading?

22. 链接器的主要任务是什么?

22. What are the principal tasks of a linker?

23. 链接器如何跨编译单元强制类型检查?

23. How can a linker enforce type checking across compilation units?

24. 动态链接的动机是什么?

24. What is the motivation for dynamic linking?

15.8 总结和结束语

15.8 Summary and Concluding Remarks

在本章中,我们将注意力集中在编译器的后端,特别是代码生成、汇编链接

In this chapter we focused our attention on the back end ofthe compiler, and on code generation, assembly, and linking in particular.

编译器的中端和后端在内部结构上差异很大。我们讨论了一种可行的结构,其中语义分析之后按顺序进行中间代码生成、与机器无关的代码改进、目标代码生成和机器特定的代码改进(包括寄存器分配和指令调度)。语义分析器将语法树传递给中间代码生成器,中间代码生成器又将控制流图传递给与机器无关的代码改进器。在控制流图的节点内,我们建议用具有无限数量虚拟寄存器的伪汇编语言指令来表示代码。为了将代码改进的讨论推迟到第 17 章,我们还提出了一种更简单的后端结构,其中放弃代码改进,提前进行简单的寄存器分配,并将中间代码和目标代码生成合并为一个阶段。这个更简单的结构为我们讨论代码生成提供了背景。

Compiler middle and back ends vary greatly in internal structure. We discussed one plausible structure, in which semantic analysis is followed by, in order, intermediate code generation, machine-independent code improvement, target code generation, and machine-specific code improvement (including register allocation and instruction scheduling). The semantic analyzer passes a syntax tree to the intermediate code generator, which in turn passes a control flow graph to the machine-independent code improver. Within the nodes of the control flow graph, we suggested that code be represented by instructions in a pseudo-assembly language with an unlimited number of virtual registers. In order to delay discussion of code improvement to Chapter 17, we also presented a simpler back-end structure in which code improvement is dropped, naive register allocation happens early, and intermediate and target code generation are merged into a single phase. This simpler structure provided the context for our discussion of code generation.

我们还讨论了中间形式 (IF)。这些可以根据其级别或机器独立性程度进行分类。在配套站点上,我们考虑了 GIMPLE 和 RTL,它们是自由软件基金会 GNU 编译器的 IF。定义良好的 IF 有助于构建编译器系列,其中一种或多种语言的前端可以与许多机器的后端配对。在许多为虚拟机编译的系统中(将在第 16 章中详细讨论),编译器会生成基于堆栈的中级 IF。虽然这种 IF 通常不适合在编译器内部使用,但它可以很简单且非常紧凑。

We also discussed intermediate forms (IFs). These can be categorized in terms of their level, or degree of machine independence. On the companion site we considered GIMPLE and RTL, the IFs of the Free Software Foundation GNU compilers. A well-defined IF facilitates the construction of compiler families, in which front ends for one or more languages can be paired with back ends for many machines. In many systems that compile for a virtual machine (to be discussed at greater length in Chapter 16), the compiler produces a stack-based medium-level IF. While not generally suitable for use inside the compiler, such an IF can be simple and very compact.

中间代码生成通常通过语法树的临时遍历来执行。与语义分析一样,该过程可以用属性语法来形式化。我们展示了一个小示例语法的一部分,并用它来生成第1 章中介绍的 GCD 程序的代码。我们顺便指出,目标代码生成通常是全部或部分自动化的,使用代码生成器,该生成器将目标机器的形式化描述作为输入,并生成对指令序列或树执行模式匹配的代码。

Intermediate code generation is typically performed via ad hoc traversal of a syntax tree. Like semantic analysis, the process can be formalized in terms of attribute grammars. We presented part of a small example grammar and used it to generate code for the GCD program introduced in Chapter 1. We noted in passing that target code generation is often automated, in whole or in part, using a code generator generator that takes as input a formal description of the target machine and produces code that performs pattern matching on instruction sequences or trees.

在讨论汇编和链接时,我们描述了可重定位可执行目标文件的格式,并讨论了名称解析重定位的概念。我们注意到,虽然并非所有编译器都包含显式汇编阶段,但所有编译系统都必须能够生成用于调试目的的汇编代码,并且必须允许程序员用汇编程序编写专用例程。在使用汇编程序的编译器中,汇编阶段有时负责指令调度和其他低级代码改进。链接器则支持单独编译,方法是将多个编译生成的目标文件“粘合”在一起。在许多现代系统中,链接任务的很大一部分被延迟到加载时间甚至运行时,允许程序共享大型流行库的代码段。对于许多语言,链接器必须执行一定量的语义检查,以保证类型一致性。在更积极的优化编译系统中(本文未讨论),链接器还可以执行过程间代码改进。

In our discussion of assembly and linking we described the format of relocatable and executable object files, and discussed the notions of name resolution and relocation. We noted that while not all compilers include an explicit assembly phase, all compilation systems must make it possible to generate assembly code for debugging purposes, and must allow the programmer to write special-purpose routines in assembler. In compilers that use an assembler, the assembly phase is sometimes responsible for instruction scheduling and other low-level code improvement. The linker, for its part, supports separate compilation, by “gluing” together object files produced by multiple compilations. In many modern systems, significant portions of the linking task are delayed until load time or even run time, to allow programs to share the code segments of large, popular libraries. For many languages the linker must perform a certain amount of semantic checking, to guarantee type consistency. In more aggressive optimizing compilation systems (not discussed in this text), the linker may also perform interprocedural code improvement.

如第 1.5 节所述,典型的编程环境包含大量附加工具,包括调试器、性能分析器、配置和版本管理器、样式检查器、预处理器、漂亮打印机、测试系统以及细读和交叉引用实用程序。其中许多工具(特别是在集成良好的环境中)都由编译器直接支持。例如,许多工具都利用了目标文件中嵌入的符号表信息。性能分析器和测试系统通常依赖于编译器在子程序调用、循环边界和代码中的其他关键点插入的特殊检测代码。细读、样式检查和漂亮打印程序可以共享编译器的扫描器和解析器。配置工具通常依赖于文件间依赖关系列表(同样由编译器生成),以告知何时对大型系统的一部分的更改可能需要重新编译其他部分。

As noted in Section 1.5, the typical programming environment includes a host of additional tools, including debuggers, performance profilers, configuration and version managers, style checkers, preprocessors, pretty-printers, testing systems, and perusal and cross-referencing utilities. Many of these tools, particularly in well-integrated environments, are directly supported by the compiler. Many make use, for example, of symbol-table information embedded in object files. Performance profilers and testing systems often rely on special instrumentation code inserted by the compiler at subroutine calls, loop boundaries, and other key points in the code. Perusal, style-checking, and pretty-printing programs may share the compiler's scanner and parser. Configuration tools often rely on lists of interfile dependences, again generated by the compiler, to tell when a change to one part of a large system may require that other parts be recompiled.

15.9 练习

15.9 Exercises

15.1 如果你正在编写一个两遍编译器,为什么你会选择高级 IF 作为前端和后端之间的连接?为什么你会选择中级 IF?

15.1 If you were writing a two-pass compiler, why might you choose a high-level IF as the link between the front end and the back end? Why might you choose a medium-level IF?

15.2考虑一种类似 Ada 或 Modula-2 语言,其中模块M可以分为规范(头文件)和实现(主体)文件,以便单独编译(第 10.2.1 节)。M的规范本身是否应该单独编译,还是应该由编译器在编译M的主体和使用M中定义的抽象的其他模块的主体的过程中直接读取它?如果规范被编译,输出应该由什么组成?

15.2 Consider a language like Ada or Modula-2, in which a module M can be divided into a specification (header) file and an implementation (body) file for the purpose of separate compilation (Section 10.2.1). Should M's specification itself be separately compiled, or should the compiler simply read it in the process of compiling M's body and the bodies of other modules that use abstractions defined in M? If the specification is compiled, what should the output consist of?

15.3 许多研究编译器(例如 SR [ AO93 ]、Cedar [ SZBH86 ]、Lynx [ Sco91 ] 和 Modula-3 [ Har92 ])都使用 C 作为其 IF。C 有详尽的文档,并且大部分与机器无关,而且 C 编译器比其他后端更广泛地可用。生成 C 有什么缺点?如何克服?

15.3 Many research compilers (e.g., for SR [AO93], Cedar [SZBH86], Lynx [Sco91], and Modula-3 [Har92]) have used C as their IF. C is well documented and mostly machine independent, and C compilers are much more widely available than alternative back ends. What are the disadvantages of generating C, and how might they be overcome?

15.4 尽可能多地列出即时编译器后端与更传统的编译器的不同之处。这些不同之处是由哪些设计目标决定的?

15.4 List as many ways as you can think of in which the back end of a just-in-time compiler might differ from that of a more conventional compiler. What design goals dictate the differences?

15.5 假设图 15.6中的k(临时寄存器的数量)为 4(对于现代机器来说,这是一个人为设定的小数字)。请给出一个表达式的例子,该表达式在我们的简单寄存器分配算法下会导致寄存器溢出。

15.5 Suppose that k (the number oftemporary registers) in Figure 15.6 is 4 (this is an artificially small number for modern machines). Give an example of an expression that will lead to register spilling under our naive register allocation algorithm.

15.6 修改图 15.6的属性文法,使得它将生成图 15.3的控制流图,而不是图 15.7的线性汇编代码。

15.6 Modify the attribute grammar of Figure 15.6 in such a way that it will generate the control flow graph of Figure 15.3 instead of the linear assembly code of Figure 15.7.

15.7向 图 15.6的文法中添加产生式和属性规则,以处理 Ada 风格的for循环(如第 6.5.1 节所述)。使用修改后的文法,将图 15.10的语法树手动翻译为伪汇编符号。将索引变量和上循环绑定在寄存器中。

15.7 Add productions and attribute rules to the grammar of Figure 15.6 to handle Ada-style for loops (described in Section 6.5.1). Using your modified grammar, hand-translate the syntax tree of Figure 15.10 into pseudo-assembly notation. Keep the index variable and the upper loop bound in registers.

f15-11-9780124104099
图 15.10 计算 N 个实数的平均值的程序的语法树和符号表。for 节点的子节点是索引变量、下界、上界和主体

15.8我们在 15.3 节中生成的代码存在一个问题(其中有很多问题),即它在运行时计算了本来可以在编译时计算的表达式的值。修改图 15.6中的语法以执行一种简单形式的常量折叠:只要运算符的两个操作数都是编译时常量,我们就应该在编译时计算该值,然后生成直接使用该值的代码。一定要考虑如何处理溢出。

15.8 One problem (of many) with the code we generated in Section 15.3 is that it computes at run time the value of expressions that could have been computed at compile time. Modify the grammar of Figure 15.6 to perform a simple form of constant folding: whenever both operands of an operator are compile-time constants, we should compute the value at compile time and then generate code that uses the value directly. Be sure to consider how to handle overflow.

15.9修改 图 15.6中的文法,以生成布尔表达式的跳转代码,如第 6.4.1 节所述。应假设短路求值(第 6.1.5 节)。

15.9 Modify the grammar of Figure 15.6 to generate jump code for Boolean expressions, as described in Section 6.4.1. You should assume short-circuit evaluation (Section 6.1.5).

15.10 我们的 GCD 程序没有使用子程序。扩展图 15.6的语法以处理没有参数的过程(可以自由地采用语法树结构上的任何合理约定)。确保为每个子程序生成适当的序言和结尾代码,并保存和恢复任何所需的临时寄存器。

15.10 Our GCD program did not employ subroutines. Extend the grammar of Figure 15.6 to handle procedures without parameters (feel free to adopt any reasonable conventions on the structure of the syntax tree). Be sure to generate appropriate prologue and epilogue code for each subroutine, and to save and restore any needed temporary registers.

15.11 图 15.6中的语法假设所有变量都是全局变量。在存在子程序的情况下,我们需要生成不同的代码(使用fp相对位移模式寻址)来访问局部变量和参数。在具有嵌套作用域的语言中,我们需要取消引用静态链(或索引显示)以访问既非局部也非全局的对象。假设我们正在编译一种具有嵌套子程序的语言,并使用静态链。修改图 15.6中的语法以生成代码来正确访问对象,而不管作用域如何。您可能会发现定义一个to_register子程序很有用,它可以生成加载给定对象的代码。一定要考虑l值和r值,以及值和结果传递的参数。

15.11 The grammar of Figure 15.6 assumes that all variables are global. In the presence of subroutines, we should need to generate different code (with fp-relative displacement mode addressing) to access local variables and parameters. In a language with nested scopes we should need to dereference the static chain (or index into the display) to access objects that are neither local nor global. Suppose that we are compiling a language with nested subroutines, and are using a static chain. Modify the grammar of Figure 15.6 to generate code to access objects correctly, regardless of scope. You may find it useful to define a to_register subroutine that generates the code to load a given object. Be sure to consider both l-values and r-values, and parameters passed by both value and result.

15-02-9780124104099 15.12–15.15 更深入。

15.12–15.15  In More Depth.

15.10 探索

15.10 Explorations

15.16 调查并描述您最常用的编译器的 IF。您能否指示编译器将其转储到您可以检查的文件中?除了编译器阶段之外,还有其他工具可以对 IF 进行操作(例如,调试器、代码改进器、配置管理器等)吗?其他语言或机器的编译器是否使用相同的 IF?

15.16 Investigate and describe the IF of the compiler you use most often. Can you instruct the compiler to dump it to a file which you can then inspect? Are there tools other than the compiler phases that operate on the IF (e.g., debuggers, code improvers, configuration managers, etc.)? Is the same IF used by compilers for other languages or machines?

15.17用你最喜欢的编程语言 实现图 15.6。定义适当的数据结构来表示语法树;然后通过临时树遍历为一些示例树生成代码。

15.17 Implement Figure 15.6 in your favorite programming language. Define appropriate data structures to represent a syntax tree; then generate code for some sample trees via ad hoc tree traversal.

15.18 扩展上一个练习的解决方案以处理各种其他语言特性。前面的练习中提到了几个有趣的选项。其他选项包括函数、一等子程序、case语句、记录、数组(特别是动态大小的数组)和迭代器。

15.18 Augment your solution to the previous exercise to handle various other language features. Several interesting options have been mentioned in earlier exercises. Others include functions, first-class subroutines, case statements, records, arrays (particularly those of dynamic size), and iterators.

15.19 找出您最喜欢的系统上有哪些工具可用于检查目标文件的内容(在 Unix 系统上,使用nmobjdump)。考虑某个由适量(比如说 3 到 6 个)编译单元组成的程序。使用适当的工具,列出每个编译单元中的导入和导出符号。然后将这些文件链接在一起。绘制一个地址图,显示各个代码段和数据段的放置位置。代码段中的哪些指令已因重定位而发生更改?

15.19 Find out what tools are available on your favorite system to inspect the content of object files (on a Unix system, use nm or objdump). Consider some program consisting of a modest number (three to six, say) of compilation units. Using the appropriate tool, list the imported and exported symbols in each compilation unit. Then link the files together. Draw an address map showing the locations at which the various code and data segments have been placed. Which instructions within the code segments have been changed by relocation?

15.20 在你最喜欢的 C++ 编译器中,调查外部符号名称中类型信息的编码。是否有奇怪的字符串在每个名称的末尾?如果可以,您能“逆向工程”生成它们的算法吗?要获取提示,请在您最喜欢的搜索引擎中输入“C++ 名称修改”。

15.20 In your favorite C++ compiler, investigate the encoding of type information in the names of external symbols. Are there strange strings of characters at the end of every name? If so, can you “reverse engineer” the algorithm used to generate them? For hints, type “C++ name mangling” into your favorite search engine.

15-02-9780124104099 15.21–15.25 更深入。

15.21–15.25  In More Depth.

15.11 书目注释

15.11 Bibliographic Notes

标准编译器教科书(例如,Aho 等人 [ ALSU07 ]、Cooper 和 Torczon [ CT04 ]、Grune 等人 [ GBJ + 12 ]、Appel [ App97 ] 或 Fischer 等人 [ FCL10 ] 编写的教科书)是有关后端编译器技术的可访问信息来源。在 Muchnick [ Muc97 ] 的文本中可以找到更详细的信息。Fraser 和 Hanson 在他们的lcc编译器 [ FH95 ]中提供了有关代码生成和(简单)代码改进的大量详细信息。

Standard compiler textbooks (e.g., those by Aho et al. [ALSU07], Cooper and Torczon [CT04], Grune et al. [GBJ+12], Appel [App97], or Fischer et al. [FCL10]) are an accessible source of information on back-end compiler technology. More detailed information can be found in the text of Muchnick [Muc97]. Fraser and Hanson provide a wealth of detail on code generation and (simple) code improvement in their lcc compiler [FH95].

RTL 和 GIMPLE 的文档记录在 gcc Internals Manual 中,可从www.gnu.org/onlinedocs获取。Java 字节码的文档记录由 Lindholm 和 Yellin [ LYBB14 ] 记录。通用中间语言的文档记录由 Miller 和 Ragsdale [ MR04 ] 记录。

RTL and GIMPLE are documented in the gcc Internals Manual, available from www.gnu.org/onlinedocs. Java bytecode is documented by Lindholm and Yellin [LYBB14]. The Common Intermediate Language is described by Miller and Ragsdale [MR04].

Ganapathi、Fischer 和 Hennessy [ GFH82 ] 以及 Henry 和 Damron [ HD89 ] 提供了自动代码生成器的早期调查。当时使用最广泛的技术基于 LR 解析,由 Glanville 和 Graham [ GG78 ] 提出。Fraser 等人 [ FHP92 ] 描述了一种基于动态规划的更简单的方法。LLVM 目标独立代码生成器的文档可在 llvm.org/docs/CodeGenerator.html 找到。

Ganapathi, Fischer, and Hennessy [GFH82] and Henry and Damron [HD89] provide early surveys of automatic code generator generators. The most widely used technique from that era was based on LR parsing, and was due to Glanville and Graham [GG78]. Fraser et al. [FHP92] describe a simpler approach based on dynamic programming. Documentation for the LLVM Target-Independent Code Generator can be found at llvm.org/docs/CodeGenerator.html.

Beck [ Bec97 ] 为本世纪初的汇编程序、链接器和软件开发工具提供了很好的介绍。Gingell 等人描述了 SPARC 体系结构和 SunOS Unix 变体的共享库的实现 [ GLDW87 ]。Ho 和 Olsson 描述了一种特别雄心勃勃的 Unix 动态链接器 [ HO91 ]。Tichy 提出了一种编译系统,通过以比源文件更细的粒度跟踪依赖关系来避免不必要的重新编译 [ Tic86 ]。

Beck [Bec97] provides a good turn-of-the-century introduction to assemblers, linkers, and software development tools. Gingell et al. describe the implementation of shared libraries for the SPARC architecture and the SunOS variant of Unix [GLDW87]. Ho and Olsson describe a particularly ambitious dynamic linker for Unix [HO91]. Tichy presents a compilation system that avoids unnecessary recompilations by tracking dependences at a granularity finer than the source file [Tic86].


1偏移量的大小意味着 ARM 上的分支在任一方向上的跳转都限制为 ≤ 32 MB。如果链接器发现目标比该距离更远,它必须生成“贴面”代码,将目标地址加载到r12(为此目的保留)中,然后执行间接分支。

1 The size of the offset implies that branches on ARM are limited to jumps of ≤ 32 MB in either direction. If the linker discovers that a target is farther away than that, it must generate “veneer” code that loads the target address into r12 (which is reserved for this purpose) and then performs and indirect branch.

16

运行时程序管理

Run-Time Program Management

高级编程语言的每一个重要实现都会大量使用库。一些库例程非常简单:它们可能会将内存从一个地方复制到另一个地方,或者执行硬件不直接支持的算术函数。其他库例程则更为复杂。例如,堆管理例程会维护大量内部状态,缓冲或图形 I/O 的库也是如此。

Every nontrivial implementation of a high-level programming language makes extensive use of libraries. Some library routines are very simple: they may copy memory from one place to another, or perform arithmetic functions not directly supported by the hardware. Others are more sophisticated. Heap management routines, for example, maintain significant amounts of internal state, as do libraries for buffered or graphical I/O.

一般来说,我们使用术语运行时系统(或有时只使用运行时,不带连字符)来指代语言实现所依赖的一组库,这些库是正确运行所必需的。运行时的某些部分(比如堆管理)从子例程参数中获取所需的所有信息,并且可以轻松地用替代实现替换。然而,其他部分则需要对编译器或生成的程序有更广泛的了解。在简单的情况下,这些知识实际上只是编译器和运行时都遵守的一组约定(例如,子例程调用序列)。在更复杂的情况下,编译器会生成程序特定的元数据,运行时必须检查这些元数据才能完成其工作。例如,跟踪垃圾收集器(第 8.5.3 节)依赖于标识程序中所有“根指针”(所有全局、静态和基于堆栈的指针或引用变量)的元数据,以及每个引用和每个分配块的类型。

In general, we use the term run-time system (or sometimes just runtime, without the hyphen) to refer to the set of libraries on which the language implementation depends for correct operation. Some parts of the runtime, like heap management, obtain all the information they need from subroutine arguments, and can easily be replaced with alternative implementations. Others, however, require more extensive knowledge of the compiler or the generated program. In simpler cases, this knowledge is really just a set of conventions (e.g., for the subroutine calling sequence) that the compiler and runtime both respect. In more complex cases, the compiler generates program-specific metadata that the runtime must inspect to do its job. A tracing garbage collector (Section 8.5.3), for example, depends on metadata identifying all the “root pointers” in the program (all global, static, and stack-based pointer or reference variables), together with the type of every reference and of every allocated block.

前面的章节讨论过许多编译器/运行时集成的例子;我们将在侧栏 16.1 中回顾这些例子。列表的长度和复杂性通常意味着编译器和运行时系统必须一起开发。

Many examples of compiler/runtime integration have been discussed in previous chapters; we review these in Sidebar 16.1. The length and complexity of the list generally means that the compiler and the run-time system must be developed together.

有些语言(尤其是 C)的运行时系统非常小:执行给定源程序所需的大多数用户级代码要么由编译器直接生成,要么包含在独立于语言的库中。其他语言具有广泛的运行时系统。例如,C# 严重依赖于由通用语言基础结构 (CLI) 标准 [ Int12a ] 定义的运行时系统。

Some languages (notably C) have very small run-time systems: most of the user-level code required to execute a given source program is either generated directly by the compiler or contained in language-independent libraries. Other languages have extensive run-time systems. C#, for example, is heavily dependent on a run-time system defined by the Common Language Infrastructure (CLI) standard [Int12a].

例 16.1

Example 16.1

CLI 作为运行时系统和虚拟机

The CLI as a run-time system and virtual machine

与任何运行时系统一样,CLI 依赖于编译器生成的数据(例如,类型描述符、异常处理程序列表以及符号表中的某些内容)。它还对以下结构做出了广泛的假设:编译器生成的代码(例如,参数传递约定、同步机制和运行时堆栈的布局)。然而,编译器和运行时之间的耦合比这更深:CLI 编程接口非常完整,可以完全隐藏底层硬件。1这样的运行时称为虚拟机。某些虚拟机(尤其是 Java 虚拟机 (JVM))是特定于语言的。其他虚拟机(包括 CLI)明确旨在用于多种语言。在开发其 CLI 版本的同时,2 Microsoft 引入了托管代码一词来指代在虚拟机之上运行的程序。

Like any run-time system, the CLI depends on data generated by the compiler (e.g., type descriptors, lists of exception handlers, and certain content from the symbol table). It also makes extensive assumptions about the structure of compiler-generated code (e.g., parameter-passing conventions, synchronization mechanisms, and the layout of run-time stacks). The coupling between compiler and runtime runs deeper than this, however: the CLI programming interface is so complete as to fully hide the underlying hardware.1 Such a runtime is known as a virtual machine. Some virtual machines—notably the Java Virtual Machine (JVM)—are language-specific. Others, including the CLI, are explicitly intended for use with multiple languages. In conjunction with development of their version of the CLI,2 Microsoft introduced the term managed code to refer to programs that run on top of a virtual machine.

设计与实现

Design & Implementation

16.1 运行时系统

16.1 Run-time systems

语言实现中许多最有趣的主题都围绕着运行时系统,并且已在前几章中介绍过。为了为虚拟机打下基础,我们在这里回顾一下这些主题。

Many of the most interesting topics in language implementation revolve around the run-time system, and have been covered in previous chapters. To set the stage for virtual machines, we review those topics here.

垃圾收集(第 8.5.3 节。如章节介绍中所述,跟踪垃圾收集器必须能够找到程序中的所有“根指针”,并识别每个引用和每个分配块的类型。压缩收集器必须能够修改程序中的每个指针。分代收集器必须能够访问由主程序中的写屏障维护的从旧到新的指针引用列表。Java 等语言的收集器必须调用适当的finalize方法。并且在支持并发或增量收集的实现中,主程序和收集器必须就某种锁定协议达成一致,以保持堆的一致性。

Garbage Collection (Section 8.5.3). As noted in the chapter introduction, a tracing garbage collector must be able to find all the “root pointers” in the program, and to identify the type of every reference and every allocated block. A compacting collector must be able to modify every pointer in the program. A generational collector must have access to a list of old-to-new pointer references, maintained by write barriers in the main program. A collector for a language like Java must call appropriate finalize methods. And in implementations that support concurrent or incremental collection, the main program and the collector must agree on some sort of locking protocol to preserve the consistency of the heap.

可变数量的参数(第 9.3.3 节。有几种语言允许程序员声明接受任意数量、任意类型的参数的函数。在 C 语言中,对va_arg(my_args, arg_type)的调用必须返回先前标识的列表my_args中的下一个参数。要找到参数,va_arg必须了解哪些参数在哪些寄存器中传递,哪些参数在堆栈上传递(使用什么样的对齐、填充和偏移)。如果va_arg的代码完全以内联方式生成,则此知识可能完全嵌入编译器中。但是,如果任何代码都在库例程中,则这些例程是编译器特定的,因此是运行时系统的(简单)一部分。

Variable Numbers of Arguments (Section 9.3.3). Several languages allow the programmer to declare functions that take an arbitrary number of arguments, of arbitrary type. In C, a call to va_arg(my_args, arg_type) must return the next argument in the previously identified list my_args. To find the argument, va_arg must understand which arguments are passed in which registers, and which arguments are passed on the stack (with what alignment, padding, and offset). If the code for va_arg is generated entirely in-line, this knowledge may be embedded entirely in the compiler. If any of the code is in library routines, however, those routines are compiler-specific, and thus a (simple) part of the run-time system.

异常处理(第 9.4 节。异常传播要求我们在控制权逃离当前子例程时“展开”堆栈。编译器可以根据每个子例程生成释放当前框架的代码。或者,释放任何给定框架的通用例程可能是运行时系统的一部分。类似地,程序中给定点附近最近的异常处理程序可由编译器生成的代码找到,该代码维护活动处理程序的堆栈,或由通用运行时例程检查在编译时生成的程序计数器到处理程序的映射表。后一种方法避免了进入和离开受保护区域(try块)时的任何运行时成本。

Exception Handling (Section 9.4). Exception propagation requires that we “unwind” the stack whenever control escapes the current subroutine. Code to deallocate the current frame may be generated by the compiler on a subroutine-by-subroutine basis. Alternatively, a general-purpose routine to deallocate any given frame may be part of the run-time system. In a similar vein, the closest exception handler around a given point in the program may be found by compiler-generated code that maintains a stack of active handlers, or by a general-purpose run-time routine that inspects a table of program-counter-to-handler mappings generated at compile time. The latter approach avoids any run-time cost when entering and leaving a protected region (try block).

事件处理(第 9.6 节。事件通常作为单线程程序中的“自发”子例程调用或并发程序中单独专用线程中的“回调”来实现。根据实现策略,它们可能能够利用编译器的知识子程序调用约定。它们还需要主程序和事件处理程序之间的同步,以保护共享数据结构的一致性。真正的异步调用(可能在任何时候中断主程序的执行)可能需要保存机器的整个寄存器集。仅在程序中明确定义的“安全点”(通过轮询实现)发生的调用可能能够保存较少的状态。在任一情况下,对不在词汇嵌套最外层的任何处理程序的调用可能需要解释闭包以建立适当的引用环境。

Event Handling (Section 9.6). Events are commonly implemented as “spontaneous” subroutine calls in a single-threaded program, or as “callbacks” in a separate, dedicated thread of a concurrent program. Depending on implementation strategy, they may be able to exploit knowledge of the compiler's subroutine calling conventions. They also require synchronization between the main program and the event handler, to protect the consistency of shared data structures. A truly asynchronous call—one that may interrupt execution of the main program at any point—may need to save the entire register set of the machine. Calls that occur only at well-defined “safe points” in the program (implemented via polling) may be able to save a smaller amount of state. In either case, calls to any handler not at the outermost level of lexical nesting may need to interpret a closure to establish the proper referencing environment.

协程和线程实现(第 9.5 节 13.2.4 节。创建协程或线程的代码必须分配和初始化堆栈、建立引用环境、执行处理未来异常所需的任何设置,并调用指定的启动例程。transfer、yield、reschedule 和 sleep_on 等例程以及任何基于调度程序同步机制)同样必须了解有关并发实现的大量细节。

Coroutine and Thread Implementation (Sections 9.5 and 13.2.4). Code to create a coroutine or thread must allocate and initialize a stack, establish a referencing environment, perform any set-up needed to handle future exceptions, and invoke a specified start-up routine. Routines like transfer, yield, reschedule, and sleep_on (as well as any scheduler-based synchronization mechanisms) must likewise understand a wealth of details about the implementation of concurrency.

远程过程调用(C-13.5.4 节)。远程过程调用 (RPC) 融合了事件和线程的方面:从服务器的角度来看,RPC 是由单独的线程响应来自客户端的请求而执行的事件。无论是内置于语言中还是通过存根编译器实现,它都需要一个运行时系统(调度程序),该系统具有对调用约定、并发性和存储管理的详细了解。

Remote Procedure Call (Section C-13.5.4). Remote procedure call (RPC) merges aspects of events and threads: from the server's point of view, an RPC is an event executed by a separate thread in response to a request from a client. Whether built into the language or implemented via a stub compiler, it requires a run-time system (dispatcher) with detailed knowledge of calling conventions, concurrency, and storage management.

事务内存(第 13.4.4 节。事务内存的软件实现必须缓冲推测更新、跟踪推测读取、检测与其他事务的冲突,并在执行任何可能因不一致而受到影响的操作之前验证其内存视图。它还必须准备好在中止时回滚其更新,或者在提交时使它们永久化。这些操作通常需要在每个事务的开始和结束时进行库调用,并且最多需要在其间进行读写指令。除其他事项外,这些调用必须了解内存中对象的布局、与对象和事务相关的元数据的含义以及在冲突事务之间进行仲裁的策略。

Transactional Memory (Section 13.4.4). A software implementation of transactional memory must buffer speculative updates, track speculative reads, detect conflicts with other transactions, and validate its view of memory before performing any operation that might be compromised by inconsistency. It must also be prepared to roll back its updates if aborted, or to make them permanent if committed. These operations typically require library calls at the beginning and end of every transaction, and at most read and write instructions in between. Among other things, these calls must understand the layout of objects in memory, the meaning of metadata associated with objects and transactions, and the policy for arbitrating between conflicting transactions.

动态链接(第 C-15.7 节。在任何具有单独编译的系统中,编译器都会生成符号表信息,链接器会使用该信息来解析外部引用。在具有完全动态(惰性)链接的系统中,外部引用(临时)填充指向链接器的指针,该指针必须成为运行时系统的一部分。当程序尝试调用尚未链接的例程时,它实际上会调用链接器,链接器会动态解析引用。具体来说,链接器查找描述要调用的例程的符号表信息。然后,它以与语言的子例程调用约定一致的方式修补将控制未来调用的链接表。

Dynamic Linking (Section C-15.7). In any system with separate compilation, the compiler generates symbol table information that the linker uses to resolve external references. In a system with fully dynamic (lazy) linking, external references are (temporarily) filled with pointers to the linker, which must then be part of the run-time system. When the program tries to call a routine that has not yet been linked, it actually calls the linker, which resolves the reference dynamically. Specifically, the linker looks up symbol table information describing the routine to be called. It then patches, in a manner consistent with the language's subroutine calling conventions, the linkage tables that will govern future calls.

虚拟机是使用编译器技术进行运行时管理和程序操作的一种日益流行的趋势的一部分。这一趋势是本章的主题。我们将在第 16.1 节中更详细地讨论虚拟机。为了避免模拟非本机指令集的开销,许多虚拟机使用即时(JIT) 编译器将其指令集转换为底层硬件的指令集。有些甚至可能在程序运行后调用编译器来编译新发现的组件或根据程序、其输入或底层系统的动态发现的属性优化代码。使用相关技术,一些语言实现执行二进制翻译以将为某台机器编译的程序重新定位到另一台机器上运行,或执行二进制重写以检测或优化已经为当前机器编译的程序。我们将在第16.2 节中讨论这些各种形式的机器代码后期绑定。最后,在第 16.3 节中,我们将讨论运行时机制来检查或修改正在运行的程序的状态。符号调试器以及分析和性能分析工具都需要这样的机制。它们还可能支持反射,这允许程序在运行时检查和推理其自身的状态。

Virtual machines are part of a growing trend toward run-time management and manipulation of programs using compiler technology. This trend is the subject of this chapter. We consider virtual machines in more detail in Section 16.1. To avoid the overhead of emulating a non-native instruction set, many virtual machines use a just-in-time (JIT) compiler to translate their instruction set to that of the underlying hardware. Some may even invoke the compiler after the program is running, to compile newly discovered components or to optimize code based on dynamically discovered properties of the program, its input, or the underlying system. Using related technology, some language implementations perform binary translation to retarget programs compiled for one machine to run on another machine, or binary rewriting to instrument or optimize programs that have already been compiled for the current machine. We consider these various forms of late binding of machine code in Section 16.2. Finally, in Section 16.3, we consider run-time mechanisms to inspect or modify the state of a running program. Such mechanisms are needed by symbolic debuggers and by profiling and performance analysis tools. They may also support reflection, which allows a program to inspect and reason about its own state at run time.

16.1 虚拟机

16.1 Virtual Machines

虚拟机( VM) 提供完整的编程环境:其应用程序编程接口 (API) 包括正确执行在其上运行的程序所需的一切。我们通常将术语“VM”保留用于抽象级别与硬件实现的计算机相当的环境。(例如,Smalltalk 或 Python 解释器通常不被描述为虚拟机,因为它的抽象级别太高,但这是一种主观称呼。)

A virtual machine (VM) provides a complete programming environment: its application programming interface (API) includes everything required for correct execution of the programs that run above it. We typically reserve use of the term “VM” to environments whose level of abstraction is comparable to that of a computer implemented in hardware. (A Smalltalk or Python interpreter, for example, is usually not described as a virtual machine, because its level of abstraction is too high, but this is a subjective call.)

每个虚拟机 API 都包含一个指令集架构 (ISA),用于表达程序。这可能与某些现有的物理机,或者它可能是一个人工指令集,旨在更易于在软件中实现并使用编译器生成。VM API 的其他部分可能支持 I/O、调度或由库或物理机的操作系统 (OS) 提供的其他服务。

Every virtual machine API includes an instruction set architecture (ISA) in which to express programs. This may be the same as the instruction set of some existing physical machine, or it may be an artificial instruction set designed to be easier to implement in software and to generate with a compiler. Other portions of the VM API may support I/O, scheduling, or other services provided by a library or by the operating system (OS) of a physical machine.

实际上,虚拟机通常被描述为系统虚拟机或进程虚拟机。系统虚拟机忠实地模拟运行标准操作系统所需的所有硬件设施,包括特权和非特权指令、内存映射 I/O、虚拟内存和中断设施。相比之下,进程虚拟机提供了单个用户级进程所需的环境:指令集的非特权子集以及 I/O 和其他服务的库级接口。

In practice, virtual machines tend to be characterized as either system VMs or process VMs. A system VM faithfully emulates all the hardware facilities needed to run a standard OS, including both privileged and unprivileged instructions, memory-mapped I/O, virtual memory, and interrupt facilities. By contrast, a process VM provides the environment needed by a single user-level process: the unprivileged subset of the instruction set and a library-level interface to I/O and other services.

系统 VM 通常由虚拟机监视器(VMM) 或虚拟机管理程序管理,它将单个物理机器多路复用到一组“客户”操作系统中,每个操作系统都在自己的虚拟机中运行。第一个广泛使用的 VMM 是 IBM 的 CP/CMS,它于 1967 年首次亮相。IBM 并没有构建一个能够支持多个用户的操作系统,而是使用 CP(“控制程序”)VMM 来创建一组虚拟机,每个虚拟机都运行一个轻量级的单用户操作系统 (CMS)。近年来,VMM 在云计算的兴起中发挥了核心作用,它允许托管中心在大量(相互隔离的)客户操作系统之间共享物理机器。如果客户工作负载在裸机上运行,​​中心可以更轻松地监控和管理其工作负载 — — 它甚至可以将正在运行的操作系统从一台机器迁移到另一台机器,以平衡客户之间的负载或清理机器以进行硬件维护。系统虚拟机在个人电脑上也越来越受欢迎,其中 VMware Fusion 和 Parallels Desktop 等产品允许用户同时在多个操作系统上运行程序。

System VMs are often managed by a virtual machine monitor (VMM) or hypervisor, which multiplexes a single physical machine among a collection of “guest” operating systems, each of which runs in its own virtual machine. The first widely available VMM was IBM's CP/CMS, which debuted in 1967. Rather than build an operating system capable of supporting multiple users, IBM used the CP (“control program”) VMM to create a collection of virtual machines, each of which ran a lightweight, single-user operating system (CMS). In recent years, VMMs have played a central role in the rise of cloud computing, by allowing a hosting center to share physical machines among a large number of (mutually isolated) guest OSes. The center can monitor and manage its workload more easily if customer workloads were running on bare hardware—it can even migrate running OSes from one machine to another, to balance load among customers or to clear machines for hardware maintenance. System VMs are also increasingly popular on personal computers, where products like VMware Fusion and Parallels Desktop allow users to run programs on top of more than one OS at once.

然而,对编程语言设计和实现影响最大的是进程虚拟机。与系统虚拟机一样,该技术已有几十年的历史:例如,示例 1.15中描述的 P 代码虚拟机可以追溯到 20 世纪 70 年代初。进程虚拟机最初被认为是一种提高程序可移植性和在新硬件上快速“引导”语言的方法。传统的缺点是由于对抽象指令集的解释而导致的性能不佳。可移植性和性能之间的权衡一直持续到 20 世纪 90 年代后期,当时 Java 的早期版本通常比 Fortran 或 C 等传统编译语言慢一个数量级。然而,随着即时编译的引入,Java 虚拟机 (JVM) 和公共语言基础结构 (CLI) 的现代实现已经可以与传统语言在本机硬件上的性能相媲美。我们将在第16.1.1和16.1.2中讨论这些系统。

It is process VMs, however, that have had the greatest impact on programming language design and implementation. As with system VMs, the technology is decades old: the P-code VM described in Example 1.15, for example, dates from the early 1970s. Process VMs were originally conceived as a way to increase program portability and to quickly “bootstrap” languages on new hardware. The traditional downside was poor performance due to interpretation of the abstract instruction set. The tradeoff between portability and performance remained valid through the late 1990s, when early versions of Java were typically an order of magnitude slower than traditionally compiled languages like Fortran or C. With the introduction of just-in-time compilation, however, modern implementations of the Java Virtual Machine (JVM) and the Common Language Infrastructure (CLI) have come to rival the performance of traditional languages on native hardware. We will consider these systems in Sections 16.1.1 and 16.1.2.

JVM 和 CLI 都使用基于堆栈的中间形式 (IF):分别是 Java 字节码和 CLI 通用中间语言 (CIL)。如第 15.2.2 节所述,缺少命名操作数意味着基于堆栈的 IF 可以非常紧凑 - 这对于通过互联网分发的代码(例如小程序)尤为重要。同时,需要按堆栈顺序计算所有内容意味着中间结果通常不能保存在寄存器中并重复使用。在许多情况下,与基于寄存器的机器的相应代码相比,基于堆栈的表达式代码将占用更少的字节,但指定更多的指令。

Both the JVM and the CLI use a stack-based intermediate form (IF): Java byte-code and CLI Common Intermediate Language (CIL), respectively. As described in Section 15.2.2, the lack of named operands means that stack-based IF can be very compact—a feature of particular importance for code (e.g., applets) distributed over the Internet. At the same time, the need to compute everything in stack order means that intermediate results cannot generally be saved in registers and reused. In many cases, stack-based code for an expression will occupy fewer bytes, but specify more instructions, than corresponding code for a register-based machine.

16.1.1 Java 虚拟机

16.1.1 The Java Virtual Machine

最终成为 Java 的语言的开发始于 1990-1991 年,当时 Sun Microsystems 的 Patrick Naughton、James Gosling 和 Mike Sheridan 开始研究嵌入式设备的编程系统。该系统的早期版本于 1992 年启动并运行,当时该语言被称为 Oak。1994 年,在尝试打入有线电视机顶盒市场失败后,该项目重新定位到 Web 浏览器,并更名为 Java。

Development of the language that eventually became Java began in 1990–1991, when Patrick Naughton, James Gosling, and Mike Sheridan of Sun Microsystems began work on a programming system for embedded devices. An early version of this system was up and running in 1992, at which time the language was known as Oak. In 1994, after unsuccessful attempts to break into the market for cable TV set-top boxes, the project was retargeted to web browsers, and the name was changed to Java.

Java 的第一个公开版本发布于 1995 年。当时,JVM 中的代码完全是解释型的。1998 年,随着 Java 2 的发布,添加了 JIT 编译器。尽管 Java 未被任何常见机构(ANSI、ISO、ECMA)标准化,但它的定义足够完善,可以接受各种编译器和 JVM。Oracle 的javac编译器和 HotSpot JVM 于 2006 年作为开源发布,是迄今为止使用最广泛的。Jikes RVM(研究虚拟机)是一种自托管 JVM,用 Java 本身编写,广泛用于 VM 研究。几家公司都有自己的专有 JVM 和类库,旨在在特定机器或特定市场上提供竞争优势。

The first public release of Java occurred in 1995. At that time code in the JVM was entirely interpreted. A JIT compiler was added in 1998, with the release of Java 2. Though not standardized by any of the usual agencies (ANSI, ISO, ECMA), Java is sufficiently well defined to admit a wide variety of compilers and JVMs. Oracle's javac compiler and HotSpot JVM, released as open source in 2006, are by far the most widely used. The Jikes RVM (Research Virtual Machine) is a self-hosting JVM, written in Java itself, and widely used for VM research. Several companies have their own proprietary JVMs and class libraries, designed to provide a competitive edge on particular machines or in particular markets.

架构摘要

Architecture Summary

JVM 提供的接口旨在成为 Java 编译器的一个有吸引力的目标。它为所有(且仅)内置和Java 语言定义的引用类型。它还强制执行明确赋值(第 6.1.3 节)和类型安全。最后,它内置了对许多 Java 语言功能和标准库包的支持,包括异常、线程、垃圾收集、反射、动态加载和安全性。

The interface provided by the JVM was designed to be an attractive target for a Java compiler. It provides direct support for all (and only) the built-in and reference types defined by the Java language. It also enforces both definite assignment (Section 6.1.3) and type safety. Finally, it includes built-in support for many of Java's language features and standard library packages, including exceptions, threads, garbage collection, reflection, dynamic loading, and security.

设计与实现

Design & Implementation

16.2 优化基于堆栈的 IF

16.2 Optimizing stack-based IF

正如我们将在C-16.1.2 节中看到的,CLI 代码并非用于解释;它几乎总是经过 JIT 编译。因此,有时需要额外的指令来捕获基于堆栈形式的表达式,这并不是一个严重的问题:相当简单的代码改进算法(将在第17 章中讨论)允许 JIT 编译器在加载时将图 15.4左侧转换为良好的机器代码。在 CLI 设计人员看来,基于堆栈的代码的简单性和紧凑性超过了代码改进的成本。对于 Java 而言,对紧凑的移动代码(例如浏览器小程序)的需求是一个引人注目的优势,即使在早期采用解释而不是 JIT 编译的实现中也是如此。

As we shall see in Section C-16.1.2, code for the CLI was not intended for interpretation; it is almost always JIT compiled. As a result, the extra instructions sometimes needed to capture an expression in stack-based form are not a serious problem: reasonably straightforward code improvement algorithms (to be discussed in Chapter 17) allow the JIT compiler to transform the left side of Figure 15.4 into good machine code at load time. In the judgment of the CLI designers, the simplicity and compactness of the stack-based code outweigh the cost of the code improvement. For Java, the need for compact mobile code (e.g., browser applets) was a compelling advantage, even in early implementations that were interpreted rather than JIT compiled.

基于堆栈的代码的更高抽象级别也增强了可移植性。三地址指令可能适合在 SPARC 机器上执行,但不适合在 x86(双地址机器)上执行。

The higher level of abstraction of stack-based code also enhances portability. Three-address instructions might be a good fit for execution on SPARC machines, but not on the x86 (a two-address machine).

当然,Java 字节码并不要求必须从 Java 源代码生成。针对 JVM 的编译器适用于许多其他语言,包括 Ruby、JavaScript、Python 和 Scheme(传统上它们都是解释型的),以及 C、Ada、Cobol 和其他传统上编译型的语言。3甚至还有汇编程序允许程序员直接编写 Java 字节码。对于编译器和汇编程序来说,主要要求是它们生成正确的类文件。这些文件具有 JVM 可以理解的特殊格式,并且必须满足各种结构和语义约束。

Of course, nothing requires that Java bytecode be produced from Java source. Compilers targeting the JVM exist for many other languages, including Ruby, JavaScript, Python, and Scheme (all ofwhich are traditionally interpreted), as well as C, Ada, Cobol, and others, which are traditionally compiled.3 There are even assemblers that allow programmers to write Java bytecode directly. The principal requirement, for both compilers and assemblers, is that they generate correct class files. These have a special format understood by the JVM, and must satisfy a variety of structural and semantic constraints.

在启动时,JVM 通常会获得包含静态方法main的类文件的名称。它将此类加载到内存中,验证它是否满足各种必需的约束,分配所有静态字段,将其链接到任何预加载的库例程,并调用程序员为类或静态字段提供的任何初始化代码。最后,它在单个线程中调用main 。其他类(初始类所需的)可以根据需要立即或延迟加载。可以通过调用Thread 类的(内置)方法创建其他线程。以下三个小节提供了有关 JVM 存储管理、类文件格式和 Java 字节码指令集的更多详细信息。

At start-up time, a JVM is typically given the name of a class file containing the static method main. It loads this class into memory, verifies that it satisfies a variety of required constraints, allocates any static fields, links it to any preloaded library routines, and invokes any initialization code provided by the programmer for classes or static fields. Finally, it calls main in a single thread. Additional classes (needed by the initial class) may be loaded either immediately or lazily on demand. Additional threads maybe created via calls to the (built-in) methods of class Thread. The three following subsections provide additional details on JVM storage management, the format of class files, and the Java bytecode instruction set.

存储管理

Storage Management

JVM 中的存储分配机制与 Java 语言的机制相似。JVM 有一个全局常量池、一组寄存器和一个用于每个线程的堆栈、一个用于保存可执行字节码的方法区和一个用于动态分配对象的堆。

Storage allocation mechanisms in the JVM mirror those of the Java language. There is a global constant pool, a set of registers and a stack for each thread, a method area to hold executable bytecode, and a heap for dynamically allocated objects.

全球数据

例 16.2

Example 16.2

“Hello, world” 的常量

Constants for “Hello, world”

方法区类似于传统可执行文件的代码(“文本”)段,如第 15.4 节所述。常量池包含程序常量以及 JVM 和其他工具所需的各种符号表信息。与方法代码一样,常量池对用户程序是只读的。每个条目都以一个字节的标记开头,该标记指示条目其余部分包含的信息类型。可能包括各种内置类型、字符串名称以及类、方法和字段引用。例如,考虑一个简单的“Hello, world”程序:

The method area is analogous to the code (“text”) segment of a traditional executable file, as described in Section 15.4. The constant pool contains both program constants and a variety of symbol table information needed by the JVM and other tools. Like the code of methods, the constant pool is read-only to user programs. Each entry begins with a one-byte tag that indicates the kind of information contained in the rest of the entry. Possibilities include the various built-in types; character-string names; and class, method, and field references. Consider, for example, the trivial “Hello, world” program:

你好{

class Hello {

 公共静态void main(String args []){

 public static void main(String args[]) {

  System.out.println(“你好,世界!”);

  System.out.println(“Hello, world!”);

 }

 }

};

};

使用 OpenJDK 的 javac 编译器进行编译时,该程序的常量池有 28 个独立条目,如图16.1所示。条目 18 包含输出字符串的文本;条目 3 表明该文本确实是 Java 字符串。许多其他条目(7、11、14、21-24、26、27)提供了文件、类、方法和字段的文本名称。其他条目(9、10、13)是类文件中其他位置结构的名称;通过指向这些条目,结构可以是自描述的。其中四个条目(8、12、25、28)是方法和字段的类型签名。在此处显示的格式中,“ V ”表示 void;“ L name ;”是完全限定类。对于方法,括号中是参数类型列表;后面是返回类型。其余大部分条目都是对类(5、6、16、19)、字段(2)和方法(1、4)的引用。最后三个条目(15、17、20)给出了字段和方法的名称和类型。如此小的程序包含如此多的信息,这源于 Java 丰富的命名结构、库类的使用以及有意保留符号表信息以支持延迟链接、反射和调试。■

When compiled with OpenJDK's javac compiler, the constant pool for this program has 28 separate entries, shown in Figure 16.1. Entry 18 contains the text of the output string; entry 3 indicates that this text is indeed a Java string. Many of the additional entries (7, 11, 14, 21–24, 26, 27) give the textual names of files, classes, methods, and fields. Others (9, 10, 13) are the names of structures elsewhere in the class file; by pointing to these entries, the structures can be self-descriptive. Four of the entries (8, 12, 25, 28) are type signatures for methods and fields. In the format shown here, “V“ indicates void; “Lname;” is a fully qualified class. For methods, parentheses surround the list of argument types; the return type follows. Most of the remaining entries are references to classes (5, 6, 16, 19), fields (2), and methods (1, 4). The final three entries (15, 17, 20) give name and type for fields and methods. The surprising amount of information for such a tiny program stems from Java's rich naming structure, the use of library classes, and the deliberate retention of symbol table information to support lazy linking, reflection, and debugging. ■

f16-01-9780124104099
图 16.1示例 16.2中程序的 JVM 常量池内容。“Asciz”条目(以零结尾的 ASCII)包含以空结尾的字符串名称。大多数其他条目将常量类型的指示与对一个或多个其他条目的引用配对。此输出由 Sun 的 javap 工具生成。
每个线程的数据

在 JVM 上运行的程序以单个线程开始。通过分配和初始化内置类Thread的新对象,然后调用其start方法来创建其他线程。每个线程都有一小组基址寄存器、一个方法调用框架堆栈和一个可选的传统堆栈,用于调用本机(非 Java)方法。

A program running on the JVM begins with a single thread. Additional threads are created by allocating and initializing a new object of the build-in class Thread, and then calling its start method. Each thread has a small set of base registers, a stack of method call frames, and an optional traditional stack on which to call native (non-Java) methods.

方法调用堆栈上的每个框架都包含一个局部变量数组、一个用于评估方法表达式的操作数堆栈和一个指向常量池的引用,该引用标识被调用方法的动态链接所需的信息。局部变量中包含了形式参数的空间。不同时处于活动状态的变量可以共享数组中的一个槽位;这意味着同一个槽位可以在不同时间用于不同类型的数据。

Each frame on the method call stack contains an array of local variables, an operand stack for evaluation of the method's expressions, and a reference into the constant pool that identifies information needed for dynamic linking of called methods. Space for formal parameters is included among the local variables. Variables that are not live at the same time can share a slot in the array; this means that the same slot may be used at different times for data of different types.

由于 Java 字节码是面向堆栈的,因此算术和逻辑指令的操作数和结果保存在当前方法框架的操作数堆栈中,而不是寄存器中。隐式地,JVM 指令集要求每个线程有四个寄存器,用于保存程序计数器和对当前框架的引用、操作数堆栈的顶部以及局部变量数组的底部。

Because Java bytecode is stack oriented, operands and results of arithmetic and logic instructions are kept in the operand stack of the current method frame, rather than in registers. Implicitly, the JVM instruction set requires four registers per thread, to hold the program counter and references to the current frame, the top of the operand stack, and the base of the local variable array.

局部变量数组和操作数堆栈中的槽位始终为 32 位宽。较小类型的数据会被填充;长整型双精度型数据各占用两个槽位。操作数堆栈所需的最大深度可由编译器静态确定,从而便于在框架中预分配空间。

Slots in the local variable array and the operand stack are always 32 bits wide. Data of smaller types are padded; long and double data take two slots each. The maximum depth required for the operand stack can be determined statically by the compiler, making it easy to preallocate space in the frame.

为了与 Java 语言的类型系统保持一致,局部变量数组或操作数堆栈中的数据始终是引用或内置标量类型的值。结构化数据(对象和数组)必须始终位于堆。它们是使用newnewarray指令动态分配的。它们通过垃圾收集自动回收。收集算法的选择由 JVM 的实现者决定。

In keeping with the type system of the Java language, a datum in the local variable array or the operand stack is always either a reference or a value of a built-in scalar type. Structured data (objects and arrays) must always lie in the heap. They are allocated, dynamically, using the new and newarray instructions. They are reclaimed automatically via garbage collection. The choice of collection algorithm is left to the implementor of the JVM.

为了便于线程间共享,Java 语言提供了与监视器等同的功能,每个对象都有一个锁和一个隐式条件变量,如第 13.4.3 节所述。JVM 为这种同步方式提供直接支持。堆中的每个对象都有一个关联的互斥锁;在典型的实现中,锁维护一组等待进入监视器的线程。此外,每个对象都有一组关联的线程,这些线程正在等待监视器的条件变量。4使用monitorenter指令获取锁,使用monitorexit指令释放锁。大多数JVM 坚持这些调用以匹配的嵌套对的形式出现,并且给定方法中获取的每个锁都应在同一方法中释放(任何正确的 Java 语言编译器都会遵循这些规则)。

To facilitate sharing among threads, the Java language provides the equivalent of monitors with a lock and a single, implicit condition variable per object, as described in Section 13.4.3. The JVM provides direct support for this style of synchronization. Each object in the heap has an associated mutual exclusion lock; in a typical implementation, the lock maintains a set of threads waiting for entry to the monitor. In addition, each object has an associated set of threads that are waiting for the monitor's condition variable.4 Locks are acquired with the monitorenter instruction and released with the monitorexit instruction. Most JVMs insist that these calls appear in matching nested pairs, and that every lock acquired within a given method be released within the same method (any correct compiler for the Java language will follow these rules).

对共享对象的访问一致性由 Java 内存模型控制,我们在第 13.3.3 节中简要介绍了该模型。非正式地说,每个线程的行为都好像它保留了堆的私有缓存。当线程释放监视器或写入易失性变量时,JVM 必须确保对该线程缓存的所有先前更新都已写回内存。当线程进入监视器或读取易失性变量时,JVM 必须(实际上)清除线程的缓存,以便后续读取会导致从内存中重新加载位置。当然,实际实现不会执行显式写回或失效;它们从硬件的缓存一致性协议提供的内存模型开始,并在需要时使用内存屏障(隔离)指令来避免不可接受的排序。

Consistency of access to shared objects is governed by the Java memory model, which we considered briefly in Section 13.3.3. Informally, each thread behaves as if it kept a private cache of the heap. When a thread releases a monitor or writes a volatile variable, the JVM must ensure that all previous updates to the thread's cache have been written back to memory. When a thread enters a monitor or reads a volatile variable, the JVM must (in effect) clear the thread's cache so that subsequent reads cause locations to be reloaded from memory. Of course, actual implementations don't perform explicit write-backs or invalidations; they start with the memory model provided by the hardware's cache coherence protocol and use memory barrier (fence) instructions where needed to avoid unacceptable orderings.

类文件

Class Files

从物理上讲,JVM 类文件以字节流的形式存储。通常,这些字节会占用操作系统提供的一些实际文件,但它们也可以很容易地成为数据库中的记录。在许多系统中,多个类文件可能组合成一个 Java 存档 ( .jar ) 文件。

Physically, a JVM class file is stored as a stream of bytes. Typically these occupy some real file provided by the operating system, but they could just as easily be a record in a database. On many systems, multiple class files maybe combined into a Java archive (.jar) file.

从逻辑上讲,类文件具有明确定义的层次结构。它以“魔法数字”(0x_cafe_babe)开头,如边栏 14.4 中所述。接下来是

Logically, a class file has a well-defined hierarchical structure. It begins with a “magic number” (0x_cafe_babe), as described in Sidebar 14.4. This is followed by

 创建该文件的 JVM 的主版本号和次版本号

 Major and minor version numbers of the JVM for which the file was created

 常量池

 The constant pool

 当前类及其超类的常量池索引

 Indices into the constant pool for the current class and its superclass

 描述类的超接口、字段和方法的表格

 Tables describing the class's superinterfaces, fields, and methods

由于 JVM 比真实机器更简洁、更抽象,因此 Java 类文件结构比典型的目标文件(第 15.4 节)更简洁、更抽象。明显缺少的是处理典型真实机器上将地址嵌入指令的多种方式所需的大量重定位信息。取而代之的是,类文件中的字节码指令包含对常量池中符号名称的引用。当代码动态链接时,这些符号名称将成为方法区域中的引用。(或者,当代码进行 JIT 编译时,它们可能会成为经过适当编码的真实机器地址。)同时,类文件包含可执行目标文件中通常没有的大量信息。示例包括类、字段和方法的访问标志(public、private、protected、static、final、synchronized、native、abstract、strictfp);内置于文件结构中的符号表信息(而不是可选附加组件);以及针对诸如抛出异常或进入或离开监视器等高级概念的特殊指令。

Because the JVM is both cleaner and more abstract than a real machine, the Java class file structure is both cleaner and more abstract than a typical object file (Section 15.4). Conspicuously missing is the extensive relocation information required to cope with the many ways that addresses are embedded into instructions on a typical real machine. In place of this, bytecode instructions in a class file contain references to symbolic names in the constant pool. These become references into the method area when code is dynamically linked. (Alternatively, they may become real machine addresses, appropriately encoded, when the code is JIT compiled.) At the same time, class files contain extensive information not typically found in an executable object file. Examples include access flags for classes, fields, and methods (public, private, protected, static, final, synchronized, native, abstract, strictfp); symbol table information that is built into the structure of the file (rather than an optional add-on); and special instructions for such high-level notions as throwing an exception or entering or leaving a monitor.

字节码

Bytecode

方法(或构造函数或类初始化程序)的字节码出现在类文件的方法表的条目中。它附带以下内容:

The bytecode for a method (or for a constructor or a class initializer) appears in an entry in the class file's method table. It is accompanied by the following:

 局部变量的数量指示,包括参数

 An indication of the number of local variables, including parameters

 操作数堆栈所需的最大深度

 The maximum depth required in the operand stack

 异常处理程序信息表,其中每一项指示

 A table of exception handler information, each entry ofwhich indicates

 此处理程序覆盖的字节码范围

 The bytecode range covered by this handler

处理 程序本身的地址(代码中的索引)

 The address (index in the code) of the handler itself

 捕获的异常类型(常量池的索引)

 The type of exception caught (an index into the constant pool)

 调试器的可选信息:具体来说,将字节码地址映射到原始源代码中的行号的表和/或指示哪个源代码变量在字节码的哪个位置占用哪个 JVM 局部变量的表。

 Optional information for debuggers: specifically, a table mapping bytecode addresses to line numbers in the original source code and/or a table indicating which source code variable(s) occupy which JVM local variables at which points in the bytecode.

指令集

Java 字节码设计得既简单又紧凑。正交性是次要的考虑因素。每条指令都以单字节操作码开头。参数(如果有)占据后续字节,其值按大端顺序给出。除了用于switch语句的两个例外,为了紧凑起见,参数是不对齐的。但大多数指令实际上不需要参数。典型的硬件对命名寄存器中的值执行算术运算,而字节码从当前方法帧的操作数堆栈中弹出参数并将结果推送到该堆栈。此外,即使是加载和存储也经常使用单个字节。例如,对于局部变量数组中的前四个条目,每个条目都有特殊的单字节整数存储指令。同样,也有特殊指令将值 -1、0、1、2、3、4 和 5 推送到操作数堆栈。

Java bytecode was designed to be both simple and compact. Orthogonality was a strictly secondary concern. Every instruction begins with a single-byte opcode. Arguments, if any, occupy subsequent bytes, with values given in big-endian order. With two exceptions, used for switch statements, arguments are unaligned, for compactness. Most instructions, however, actually don't need an argument. Where typical hardware performs arithmetic on values in named registers, bytecode pops arguments from, and pushes result to, the operand stack of the current method frame. Moreover, even loads and stores can often use a single byte. There are, for example, special one-byte integer store instructions for each of the first four entries in the local variable array. Similarly, there are special instructions to push the values −1, 0, 1, 2, 3, 4, and 5 onto the operand stack.

从 Java 8 开始,JVM 定义了 256 个可能的操作码值中的 205 个。其中五个用于特殊用途(未使用、nop、调试器断点、实现相关)。其余可分为以下类别:

As of Java 8, the JVM defines 205 of the 256 possible opcode values. Five of these serve special purposes (unused, nop, debugger breakpoints, implementation dependent). The remainder can be organized into the following categories:

加载/存储:在操作数堆栈和局部变量数组之间来回移动值。

Load/store: move values back and forth between the operand stack and the local variable array.

算术:对操作数堆栈中的值执行整数或浮点运算。

Arithmetic: perform integer or floating point operations on values in the operand stack.

类型转换:内置类型(byte、char、short、int、long、float 和 double)之间的“加宽”或“缩小”值。缩小可能会导致精度损失,但绝不会引发异常。

Type conversion: “widen” or “narrow” values among the built-in types (byte, char, short, int, long, float, and double). Narrowing may result in a loss of precision but never an exception.

对象管理:创建或查询对象和数组的属性;访问字段和数组元素。

Object management: create or query the properties of objects and arrays; access fields and array elements.

操作数堆栈管理:压入和弹出;复制;交换。

Operand stack management: push and pop; duplicate; swap.

控制转移:执行条件、无条件或者多路分支(switch)。

Control transfer: perform conditional, unconditional, or multiway branches (switch).

方法调用:从类和接口的普通方法和静态方法(包括构造函数和初始化程序)调用和返回。Java 7 JVM 中引入的invokedynamic指令允许在运行时自定义各个调用站点的链接约定。它既可用于Java 8 lambda表达式,也可用于在JVM之上实现动态类型语言。

Method calls: call and return from ordinary and static methods (including constructors and initializers) of classes and interfaces. An invokedynamic instruction, introduced in the Java 7 JVM, allows run-time customization of linkage conventions for individual call sites. It is used both for Java 8 lambda expressions and for the implementation of dynamically typed languages on top of the JVM.

异常: throw ( catch不需要指令)。

Exceptions: throw (no instructions required for catch).

监视器:进入和退出(通过方法调用来调用wait、notifynotifyAll)。

Monitors: enter and exit (wait, notify, and notifyAll are invoked via method calls).

例 16.3

Example 16.3

列表插入操作的字节码

Bytecode for a list insert operation

作为一个具体的例子,考虑以下整数集的定义,表示为一个排序的链接列表:

As a concrete example, consider the following definitions for an integer set, represented as a sorted linked list:

公共类 LLset {

public class LLset {

 节点头;

 node head;

 类节点 {

 class node {

  公共 int val;

  public int val;

  公共节点下一步;

  public node next;

 }

 }

 public LLset() {         // 构造函数

 public LLset() {        // constructor

  head = new node();         // 头节点不包含实际数据

  head = new node();        // head node contains no real data

  head.next = 空;

  head.next = null;

 }

 }

 

 

}

}

图 16.2显示了此类的插入方法。左侧是 Java 源代码;右侧是相应字节码的符号表示。字节码顶部的行表示操作数堆栈的最大深度为 3,局部变量数组中有 4 个条目,其中前两个是参数:this指针和整数v。仔细查看代码会发现许多特殊的单字节加载和存储指令的示例,以及对操作数堆栈进行隐式操作的指令。■

An insert method for this class appears in Figure 16.2. Java source is on the left; a symbolic representation of the corresponding bytecode is on the right. The line at the top of the bytecode indicates a maximum depth of 3 for the operand stack and four entries in the local variable array, the first two of which are arguments: the this pointer and the integer v. Perusal of the code reveals numerous examples of the special one-byte load and store instructions, and of instructions that operate implicitly on the operand stack. ■

f16-02-9780124104099
图 16.2 列表插入方法的 Java 源代码和字节码右侧的输出是由 Oracle 的 javac(编译器)和 javap(反汇编器)工具生成的,其中附加了手动插入的注释。
确认

安全性是 Java 语言和虚拟机定义中的主要考虑因素之一。执行从更传统的语言编译的机器代码时可能“出错”的许多事情在执行从 Java 编译的字节码时不会出错。安全性的某些方面是通过限制字节码指令集的表现力或在加载时检查属性来实现的。例如,不能跳转到不存在的地址,因为方法调用通过名称以符号形式指定其目标,而分支目标则被指定为当前方法的代码属性中的索引。同样,当硬件允许从帧指针进行位移寻址以访问当前堆栈帧之外的内存时,JVM 会在加载时进行检查,以确保对局部变量的引用(由局部变量数组中的常量索引指定)在声明的范围内。

Safety was one of the principal concerns in the definition of the Java language and virtual machine. Many of the things that can “go wrong” while executing machine code compiled from a more traditional language cannot go wrong when executing bytecode compiled from Java. Some aspects of safety are obtained by limiting the expressiveness of the byte-code instruction set or by checking properties at load time. One cannot jump to a nonexistent address, for example, because method calls specify their targets symbolically by name, and branch targets are specified as indices within the code attribute of the current method. Similarly, where hardware allows displacement addressing from the frame pointer to access memory outside the current stack frame, the JVM checks at load time to make sure that references to local variables (specified by constant indices into the local variable array) are within the bounds declared.

JVM 在执行期间保证了其他方面的安全性。如果给定了空引用,字段访问和方法调用指令将引发异常。同样,如果索引不在数组的边界内,数组加载和存储指令也会引发异常。

Other aspects of safety are guaranteed by the JVM during execution. Field access and method call instructions throw an exception if given a null reference. Similarly, array load and store instructions throw an exception if the index is not within the bounds of the array.

首次加载类文件时,JVM 会检查文件的顶层结构。除其他事项外,它还会验证文件是否以适当的“魔法数字”开头,文件各个部分的指定大小是否都在界限之内,以及这些大小加起来是否等于整个文件的大小。将类文件链接到程序的其余部分时,JVM 还会检查其他约束。它会验证常量池中的所有项目是否格式正确,并且没有任何东西从最终类继承更重要的是,它会对类方法的字节码执行一系列检查。除其他事项外,字节码验证器还会确保每个变量在读取之前都已初始化,每个操作都是类型安全的,并且方法的操作数堆栈永远不会溢出或下溢。所有这三项检查都需要数据流分析来确定所需的属性(初始化状态、本地堆栈框架中的插槽类型、操作数堆栈的深度)在程序中给定点的每条可能路径上都是相同的。我们将在 C-17.4 节中更详细地考虑数据流。

When it first loads a class file, the JVM checks the top-level structure of the file. Among other things, it verifies that the file begins with the appropriate “magic number,” that the specified sizes of the various sections of the file are all within bounds, and that these sizes add up to the size of the overall file. When it links the class file into the rest of the program, the JVM checks additional constraints. It verifies that all items in the constant pool are well formed, and that nothing inherits from a final class. More significantly, it performs a host of checks on the bytecode of the class's methods. Among other things, the bytecode verifier ensures that every variable is initialized before it is read, that every operation is type-safe, and that the operand stacks of methods never overflow or underflow. All three of these checks require data flow analysis to determine that desired properties (initialization status, types of slots in the local stack frame, depth of the operand stack) are the same on every possible path to a given point in the program. We will consider data flow in more detail in Section C-17.4.

设计与实现

Design & Implementation

16.3 类文件和字节码的验证

16.3 Verification of class files and bytecode

Java 编译器必须生成满足 Java 类文件规范定义的所有约束的代码。这些约束包括内部数据结构的格式正确性、类型安全、明确赋值以及操作数堆栈中没有下溢或上溢。但是,JVM 无法判断给定的类文件是否由正确的编译器生成。为了保护自己免受可能不正确(甚至是恶意)的类文件的侵害,JVM 必须验证其运行的任何代码是否遵循所有规则。在正常操作下,这意味着某些检查(例如,明确赋值的数据流)会执行两次:一次由 Java 编译器执行,以向程序员提供编译时错误消息;另一次由 JVM 执行,以防止出现错误的编译器或其他字节码源。

Java compilers are required to generate code that satisfies all the constraints defined by the Java class file specification. These include well-formedness of the internal data structures, type safety, definite assignment, and lack of underflow or overflow in the operand stack. A JVM, however, has no way to tell whether a given class file was generated by a correct compiler. To protect itself from potentially incorrect (or even malicious) class files, a JVM must verify that any code it runs follows all the rules. Under normal operation, this means that certain checks (e.g., data flow for definite assignment) are performed twice: once by the Java compiler, to provide compile-time error messages to the programmer, and again by the JVM, to protect against buggy compilers or alternative sources of bytecode.

为了缩短程序启动时间并避免不必要的工作,大多数 JVM 会延迟加载(和验证)类文件,直到实际调用该文件中的某个方法(这相当于第C-15.7.2 节中描述的延迟链接)。为了实现此延迟,JVM 必须等到调用发生后才能验证调用站点上代码的最后几个属性(即,它引用的方法确实存在,并且允许调用者调用)。

To improve program start-up times and avoid unnecessary work, most JVMs delay the loading (and verification) of class files until some method in that file is actually called (this is the Java equivalent of the lazy linking described in Section C-15.7.2). In order to effect this delay, the JVM must wait until a call occurs to verify the last few properties of the code at the call site (i.e., that it refers to a method that really exists, and that the caller is allowed to call).

16.1.2 公共语言基础设施

16.1.2 The Common Language Infrastructure

早在 20 世纪 80 年代中期,微软就认识到在 Windows 平台上运行的编程语言之间需要互操作性。在一系列通过十五年的产品供应,该公司开发了日益复杂的组件对象模型 (COM) 版本,首先可以与用多种语言编写的程序组件进行通信,然后调用,最后共享数据。

As early as the mid-1980s, Microsoft recognized the need for interoperability among programming languages running on Windows platforms. In a series of product offerings spanning a decade and a half, the company developed increasingly sophisticated versions of its Component Object Model (COM), first to communicate with, then to call, and finally to share data with program components written in multiple languages.

随着 Java 的成功,到 20 世纪 90 年代中后期,人们清楚地认识到,将 JVM 风格的运行时系统与 COM 的语言互操作性相结合的系统可能具有巨大的技术和商业潜力。微软的 .NET 项目旨在实现这一潜力。它包括一个类似 JVM 的虚拟机,其规范 — 通用语言基础结构 (CLI) — 由 ECMA 和 ISO 标准化。虽然 CLI 的开发显然是由微软推动的,但其他实现 — 尤其是由 Xamarin, Inc. 领导的开源 Mono 项目 — 也可用于非 Windows 平台。

With the success of Java, it became clear by the mid to late 1990s that a system combining a JVM-style run-time system with the language interoperability of COM could have enormous technical and commercial potential. Microsoft's .NET project set out to realize this potential. It includes a JVM-like virtual machine whose specification—the Common Language Infrastructure (CLI)—is standardized by ECMA and the ISO. While development of the CLI has clearly been driven by Microsoft, other implementations—notably from the open-source Mono project, led by Xamarin, Inc.—are available for non-Windows platforms.

16-02-9780124104099 更深入地

IN MORE DEPTH

我们在配套网站上更详细地讨论了 CLI。除其他内容外,我们还描述了通用类型系统(它控制跨语言互操作性)、虚拟机的体系结构(包括其对泛型的支持)、通用中间语言 (CIL)(Java 字节码的 CLI 类似物)以及可移植可执行 (PE)程序集( .jar文件的 CLI 类似物)。

We consider the CLI in more detail on the companion site. Among other things, we describe the Common Type System, which governs cross-language interoperability; the architecture of the virtual machine, including its support for generics; the Common Intermediate Language (CIL—the CLI analogue of Java bytecode); and Portable Executable (PE) assemblies, the CLI analogue of .jar files.

16-01-9780124104099检查你的理解

Check Your Understanding

1. 什么是运行时系统?它与“单纯的”库有何不同?

1. What is a run-time system? How does it differ from a “mere” library?

2. 列出运行时系统可能执行的一些主要任务。

2. List some of the major tasks that may be performed by a run-time system.

3. 什么是虚拟机?它与其他类型的解释器有何区别?

3. What is a virtual machine?. What distinguishes it from interpreters of other sorts?

4.解释 系统虚拟机和进程虚拟机之间的区别。有时还用哪些术语来描述系统虚拟机?

4. Explain the distinction between system and process VMs. What other terms are sometimes used for system VMs?

5. 什么是托管代码

5. What is managed code?

6. 为什么很多虚拟机采用基于堆栈的中间形式?

6. Why do many virtual machines use a stack-based intermediate form?

7. 给出几个Java字节码提供的专用指令的例子。

7. Give several examples of special-purpose instructions provided by Java byte-code.

8. 总结Java虚拟机的体系结构。

8. Summarize the architecture of the Java Virtual Machine.

9. 总结Java类文件的内容。

9. Summarize the content of a Java class file.

10. 解释在加载时对类文件执行的有效性检查。

10. Explain the validity checks performed on a class file at load time.

16.2 机器代码的后期绑定

16.2 Late Binding of Machine Code

在传统概念中(示例 1.7),编译是一次性活动,与程序执行截然不同。编译器生成目标程序(通常为机器语言),随后可针对许多不同的输入执行多次。

In the traditional conception (Example 1.7), compilation is a one-time activity, sharply distinguished from program execution. The compiler produces a target program, typically in machine language, which can subsequently be executed many times for many different inputs.

但在某些环境中,将编译和执行在时间上缩短在一起是有意义的。即时(JIT) 编译器在每次运行程序之前立即将程序从源代码或中间形式转换为机器语言。我们将在下面的第一小节中进一步考虑 JIT 编译。我们还考虑了在程序开始执行后可能会编译程序新部分或重新编译旧部分的语言系统。在第16.2.216.2.3节中,我们考虑了二进制翻译二进制重写系统,它们无需访问源代码即可对程序执行类似编译器的操作。最后,在第 16.2.4 节中,我们考虑了可以从远程位置下载程序组件的系统。所有这些系统都用于延迟程序与其机器代码的绑定。

In some environments, however, it makes sense to bring compilation and execution closer together in time. A just-in-time (JIT) compiler translates a program from source or intermediate form into machine language immediately before each separate run of the program. We consider JIT compilation further in the first subsection below. We also consider language systems that may compile new pieces of a program—or recompile old pieces—after the program begins its execution. In Sections 16.2.2 and 16.2.3, we consider binary translation and binary rewriting systems, which perform compiler-like operations on programs without access to source code. Finally, in Section 16.2.4, we consider systems that may download program components from remote locations. All these systems serve to delay the binding of a program to its machine code.

16.2.1 即时编译和动态编译

16.2.1 Just-in-Time and Dynamic Compilation

为了推广 Java 语言和虚拟机,Sun Microsystems 提出了“一次编写,随处运行”的口号,其理念是让以 Java 字节码形式分发的程序可以在各种各样的平台上运行。当然,源代码也是可移植的,但字节码紧凑得多,并且可以在不需要额外预处理的情况下进行解释。不幸的是,解释的开销往往很大。在早期 Java 实现上运行的程序可能比其他语言中编译后的代码慢一个数量级。即时编译是一种在提高执行速度的同时保留字节码可移植性的技术。与解释和动态链接(第 15.7 节)一样,JIT 编译也受益于程序组件的延迟发现:程序代码不会因广泛共享的库的副本而变得臃肿,并且在运行需要库的程序时会自动获取新版本的库。

To promote the Java language and virtual machine, Sun Microsystems coined the slogan “write once, run anywhere”—the idea being that programs distributed as Java bytecode could run on a very wide range of platforms. Source code, of course, is also portable, but byte code is much more compact, and can be interpreted without additional preprocessing. Unfortunately, interpretation tends to be expensive. Programs running on early Java implementations could be as much as an order of magnitude slower than compiled code in other languages. Just-in-time compilation is, to first approximation, a technique to retain the portability of bytecode while improving execution speed. Like both interpretation and dynamic linking (Section 15.7), JIT compilation also benefits from the delayed discovery of program components: program code is not bloated by copies of widely shared libraries, and new versions of libraries are obtained automatically when a program that needs them is run.

由于 JIT 系统在执行之前立即编译程序,因此它可能会显著延迟程序的启动时间。实现者面临着一个艰难的权衡:为了最大限度地提高解释方面的效益,编译器应该生成良好的代码;为了最大限度地缩短启动时间,它应该非常快速地生成代码。一般来说,JIT 编译器倾向于专注于更简单的目标代码改进形式。具体来说,它们通常将自己限制在所谓的局部改进上,这些改进在单个控制流构造内运行。考虑全局(整个方法)和过程间(整个程序)级别的改进可能代价高昂。

Because a JIT system compiles programs immediately prior to execution, it can add significant delay to program start-up time. Implementors face a difficult tradeoff: to maximize benefits with respect to interpretation, the compiler should produce good code; to minimize start-up time, it should produce that code very quickly. In general, JIT compilers tend to focus on the simpler forms of target code improvement. Specifically, they often limit themselves to the so-called local improvements, which operate within individual control-flow constructs. Improvements at the global (whole method) and interprocedural (whole program) level may be expensive to consider.

幸运的是,由于存在较早的源代码到字节码编译器,JIT 编译的成本通常会降低,因为该编译器可以完成大部分“繁重工作”。5 JIT编译器不需要扫描,因为字节码不是文本。解析很简单,因为类文件具有简单的自描述结构。源代码到字节码编译器必须以很大的代价推断的许多属性(类型安全、实际和形式参数列表的一致性)直接嵌入在字节码的结构中(对象用其类型标记,调用通过方法描述符进行);其他属性可以通过简单的数据流分析进行验证。源代码到字节码编译器还可以执行某些形式的独立于机器的代码改进(这些改进在一定程度上受到基于堆栈的表达式评估的限制)。

Fortunately, the cost of JIT compilation is typically lessened by the existence of an earlier source-to-byte-code compiler that does much of the “heavy lifting.”5 Scanning is unnecessary in a JIT compiler, since bytecode is not textual. Parsing is trivial, since class files have a simple, self-descriptive structure. Many of the properties that a source-to-byte-code compiler must infer at significant expense (type safety, agreement of actual and formal parameter lists) are embedded directly in the structure of the bytecode (objects are labeled with their type, calls are made through method descriptors); others can be verified with simple data flow analysis. Certain forms of machine-independent code improvement may also be performed by the source-to-byte-code compiler (these are limited to some degree by stack-based expression evaluation).

所有这些因素使得 JIT 编译器比人们最初预期的运行速度更快,并且生成的代码质量更高。此外,由于我们已经承诺在运行时调用 JIT 编译器,因此我们可以通过一次运行一点而不是一次性运行来最大限度地减少其对程序启动延迟的影响:

All these factors allow a JIT compiler to be faster—and to produce better code—than one might initially expect. In addition, since we are already committed to invoking the JIT compiler at run time, we can minimize its impact on program start-up latency by running it a bit at a time, rather than all at once:

 与惰性链接器(第 C-15.7.2 节)类似,JIT 编译器可以逐步执行其工作。它首先只编译包含程序入口点(即main)的类文件,在代码中留下钩子,当程序应该调用另一个类文件中的方法时,这些钩子会调用运行时系统。经过这一小段准备后,程序开始执行。当执行通过未解析的钩子进入运行时时,运行时将调用编译器来加载新的类文件并将其链接到程序中。

 Like a lazy linker (Section C-15.7.2), a JIT compiler may perform its work incrementally. It begins by compiling only the class file that contains the program entry point (i.e., main), leaving hooks in the code that call into the run-time system wherever the program is supposed to call a method in another class file. After this small amount of preparation, the program begins execution. When execution falls into the runtime through an unresolved hook, the runtime invokes the compiler to load the new class file and to link it into the program.

 为了消除编译原始类文件的延迟,语言实现可能同时包含解释器和 JIT 编译器。执行从解释器开始。同时,编译器将程序的各部分翻译成机器码。当解释器需要调用方法时,它会检查编译版本是否可用,如果有,则调用该版本而不是解释字节码。我们将在下文中在 HotSpot Java 编译器和 JVM 的上下文中回顾这项技术。

 To eliminate the latency of compiling even the original class file, the language implementation may incorporate both an interpreter and a JIT compiler. Execution begins in the interpreter. In parallel, the compiler translates portions of the program into machine code. When the interpreter needs to call a method, it checks to see whether a compiled version is available yet, and if so calls that version instead of interpreting the bytecode. We will return to this technique below, in the context of the HotSpot Java compiler and JVM.

 当类文件经过 JIT 编译时,语言实现可以缓存生成的机器代码以供以后使用。这相当于推测程序当前运行中使用的库例程版本在程序再次运行时仍将是最新的。由于 Java 和 C# 等语言需要库例程的后期绑定,因此必须在每次后续运行中检查此猜测。如果检查成功,使用缓存副本几乎可以节省 JIT 编译的全部成本。

 When a class file is JIT compiled, the language implementation can cache the resulting machine code for later use. This amounts to guessing, speculatively, that the versions of library routines employed in the current run of the program will still be current when the program is run again. Because languages like Java and C# require the appearance of late binding of library routines, this guess must be checked in each subsequent run. If the check succeeds, using a cached copy saves almost the entire cost of JIT compilation.

最后,JIT 编译提供了执行某些类型的代码改进的机会,而这些改进在传统编译器中通常是不可行的。例如,软件供应商通常会发布一个编译版本的对于给定的指令集架构,JIT 编译器可以针对特定应用程序优化,即使该架构的实现可能在重要方面有所不同,包括管道宽度和深度、物理(重命名)寄存器的数量以及各级缓存的数量、大小和速度。JIT 编译器可能能够识别其所运行的处理器实现,并生成针对该特定实现进行调整的代码。更重要的是,JIT 编译器可能能够内联对动态链接库例程的调用。这种优化在面向对象的程序中尤为重要,因为面向对象的程序往往会调用许多小方法。对于此类程序,动态内联会对性能产生巨大影响。

Finally, JIT compilation affords the opportunity to perform certain kinds of code improvement that are usually not feasible in traditional compilers. It is customary, for example, for software vendors to ship a single compiled version of an application for a given instruction set architecture, even though implementations of that architecture may differ in important ways, including pipeline width and depth; the number of physical (renaming) registers; and the number, size, and speed of the various levels of cache. A JIT compiler may be able to identify the processor implementation on which it is running, and generate code that is tuned for that specific implementation. More important, a JIT compiler may be able to in-line calls to dynamically linked library routines. This optimization is particularly important in object-oriented programs, which tend to call many small methods. For such programs, dynamic in-lining can have a dramatic impact on performance.

动态编译

Dynamic Compilation

我们注意到,语言实现可能会选择延迟 JIT 编译,以减少对程序启动延迟的影响。在某些情况下,必须延迟编译,因为源代码或字节码直到运行时才创建或发现,或者因为我们希望执行依赖于执行期间收集的信息的优化。在这些情况下,我们说语言实现采用了动态编译。Common Lisp 系统已经使用动态编译很多年了:语言通常是编译的,但程序可以在运行时扩展自身。基于运行时统计的优化是一项较新的创新。

We have noted that a language implementation may choose to delay JIT compilation to reduce the impact on program start-up latency. In some cases, compilation must be delayed, either because the source or bytecode was not created or discovered until run time, or because we wish to perform optimizations that depend on information gathered during execution. In these cases, we say the language implementation employs dynamic compilation. Common Lisp systems have used dynamic compilation for many years: the language is typically compiled, but a program can extend itself at run time. Optimization based on run-time statistics is a more recent innovation.

大多数程序的大部分时间都花在代码的一小部分上。对这一部分进行积极的代码改进可以大大提高程序性能。动态编译器可以使用运行时分析收集的统计数据来识别代码中的热路径,然后在后台对其进行优化。通过重新排列代码以使热路径在内存中连续,它还可以提高指令缓存的性能。额外的运行时统计数据可能会建议展开循环(练习 C-5.21)、将常用表达式分配给寄存器(第 C-5.5.2 和 C-17.8 节)以及调度指令以最大限度地减少流水线停顿(第 C-5.5.1 和 C-17.6 节)的机会。

Most programs spend most of their time in a relatively small fraction of the code. Aggressive code improvement on this fraction can yield disproportionately large improvements in program performance. A dynamic compiler can use statistics gathered by run-time profiling to identify hot paths through the code, which it then optimizes in the background. By rearranging the code to make hot paths contiguous in memory, it may also improve the performance of the instruction cache. Additional run-time statistics may suggest opportunities to unroll loops (Exercise C-5.21), assign frequently used expressions to registers (Sections C-5.5.2 and C-17.8), and schedule instructions to minimize pipeline stalls (Sections C-5.5.1 and C-17.6).

例 16.4

Example 16.4

内联什么时候是安全的?

When is in-lining safe?

在某些情况下,动态编译器甚至可以执行静态实现时不安全的优化。例如,考虑动态链接库中方法的内联。如果foo是类C的静态方法,则可以安全地内联对C.foo的调用。类似地,如果bar是类C的final方法(不能被覆盖),而o是类C的对象,则可以安全地内联对o.bar的调用。但如果bar不是final呢?如果编译器可以证明o永远不会引用从C派生的类的实例(可能具有不同的bar实现),它仍然可以内联对o.bar的调用。有时这很容易:

In some situations, a dynamic compiler may even be able to perform optimizations that would be unsafe if implemented statically. Consider, for example, the in-lining of methods from dynamically linked libraries. If foo is a static method of class C, then calls to C.foo can safely be in-lined. Similarly, if bar is a final method of class C (one that cannot be overridden), and o is an object of class C, then calls to o.bar can safely be in-lined. But what if bar is not final? The compiler can still in-line calls to o.bar if it can prove that o will never refer to an instance of a class derived from C (which might have a different implementation of bar). Sometimes this is easy:

C o = new C(参数);

C o = new C( args );

o.bar();     // 毫无疑问这是什么类型

o.bar();    // no question what type this is

其他时候则不然:

Other times it is not:

静态 void f(C o) {

static void f(C o) {

 o.bar();

 o.bar();

}

}

这里,只有当编译器知道f永远不会被传递给从C派生的类的实例时,它才能内联调用。如果动态编译器验证程序(当前版本)中不存在从C派生的类,它就可以执行优化。但是,它必须记录它所做的事情:如果动态链接随后使用定义从C派生的新类D 的代码扩展程序,则可能需要撤消内联优化。■

Here the compiler can in-line the call only if it knows that f will never be passed an instance of a class derived from C. A dynamic compiler can perform the optimization if it verifies that there exists no class derived from C anywhere in the (current version of the) program. It must keep notes of what it has done, however: if dynamic linking subsequently extends the program with code that defines a new class D derived from C, the in-line optimization may need to be undone. ■

例 16.5

Example 16.5

推测优化

Speculative optimization

在某些情况下,动态编译器可能会选择执行即使在当前程序中也可能不安全的优化,前提是性能分析表明这些优化是有益的,并且运行时检查可以确定它们是否安全。假设在上例中,已经存在一个从C派生的类D,但性能分析表明到目前为止对f的每次调用都传递了类C的实例。进一步假设f对参数o的方法进行了多次调用,而不仅仅是示例中显示的方法。编译器可能会选择生成以下代码:

In some cases, a dynamic compiler may choose to perform optimizations that may be unsafe even in the current program, provided that profiling suggests they will be profitable and run-time checks can determine whether they are safe. Suppose, in the previous example, there already exists a class D derived from C, but profiling indicates that every call to f so far has passed an instance of class C. Suppose further that f makes many calls to methods of parameter o, not just the one shown in the example. The compiler might choose to generate code along the following lines:

静态 void f(C o) {

static void f(C o) {

 如果 (o.getClass() == C.class) {

 if (o.getClass() == C.class) {

  … // 使用内联调用的代码 — 速度更快

  … // code with in-lined calls — much faster

 } 别的 {

 } else {

  … // 没有内联调用的代码

  … // code without in-lined calls

 }

 }

}

}

示例系统:HotSpot Java 编译器

An Example System: the HotSpot Java Compiler

HotSpot 是 Oracle 针对桌面和服务器系统的主要 JVM 和 JIT 编译器。它于 1999 年首次发布,并以开源形式提供。

HotSpot is Oracle's principal JVM and JIT compiler for desktop and server systems. It was first released in 1999, and is available as open source.

HotSpot 的名称源于它使用动态编译来提高热代码路径的性能。新加载的类文件最初被解释。JVM 选择经常执行的方法进行编译,随后动态修补到程序中。编译器会积极地内联小例程,并将以深度、迭代的方式进行,反复内联从刚完成内联的代码中调用的例程。如前面关于动态编译的讨论中所述,编译器还将内联仅对当前类文件集安全的例程,并将动态“取消优化”由于加载新的派生类而变得不安全的内联调用。

HotSpot takes its name from its use of dynamic compilation to improve the performance of hot code paths. Newly loaded class files are initially interpreted. Methods that are executed frequently are selected by the JVM for compilation and are subsequently patched into the program on the fly. The compiler is aggressive about in-lining small routines, and will do so in a deep, iterative fashion, repeatedly in-lining routines that are called from the code it just finished in-lining. As described in the preceding discussion of dynamic compilation, the compiler will also in-line routines that are safe only for the current set of class files, and will dynamically “deoptimize” in-lined calls that have been rendered unsafe by the loading of new derived classes.

HotSpot 编译器可以配置为在“客户端”或“服务器”模式下运行。客户端模式针对较低的启动延迟进行了优化。它适用于人类用户经常启动新程序的系统。它将 Java 字节码转换为静态单赋值 (SSA) 形式(第 C-17.4.1 节中描述的中级 IF),并执行一些简单的与机器无关的优化。然后,它转换为低级 IF,并对其执行指令调度和寄存器分配。最后,它将这个IF翻译成机器码。

The HotSpot compiler can be configured to operate in either “client” or “server” mode. Client mode is optimized for lower start-up latency. It is appropriate for systems in which a human user frequently starts new programs. It translates Java bytecode to static single assignment (SSA) form (a medium-level IF described in Section C-17.4.1) and performs a few straightforward machine-independent optimizations. It then translates to a low-level IF, on which it performs instruction scheduling and register allocation. Finally, it translates this IF to machine code.

服务器模式经过优化,可生成更快的代码。它适用于需要最大吞吐量且可以容忍较慢启动速度的系统。它将大多数经典的全局和过程间代码改进技术应用于程序的 SSA 版本(其中许多技术在第17 章中描述),以及其他特定于 Java 的改进。其中许多改进都利用了分析统计数据。

Server mode is optimized to generate faster code. It is appropriate for systems that need maximum throughput and can tolerate slower start-up. It applies most classic global and interprocedural code improvement techniques to the SSA version of the program (many of these are described in Chapter 17), as well as other improvements specific to Java. Many of these improvements make use of profiling statistics.

特别是在服务器模式下运行时,HotSpot 的性能可与传统的 C 和 C++ 编译器相媲美。实际上,积极的内联和配置文件驱动的优化可以“挽回” JIT 编译的启动延迟和 Java 运行时语义检查的开销。

Particularly when running in server mode, HotSpot can rival the performance of traditional compilers for C and C++. In effect, aggressive in-lining and profile-driven optimization serve to “buy back” both the start-up latency of JIT compilation and the overhead of Java's run-time semantic checks.

其他示例系统

Other Example Systems

例 16.6

Example 16.6

CLR 中的动态编译

Dynamic compilation in the CLR

与 HotSpot 一样,Microsoft 的 CIL 到机器代码编译器对热代码路径进行动态优化。.NET 源代码到 CIL 编译器也可通过 System.CodeDom.Compiler API明确供程序使用。在 CLR 上运行的程序可以直接调用编译器将 C#(或 Visual Basic 或其他 .NET 语言)转换为 CIL PE 程序集。然后可以将它们加载到正在运行的程序中。加载后,CLR JIT 编译器会将它们转换为机器代码。如第3.6.4 节第 11.2节所述,C# 包含的 lambda 表达式让人联想到函数式语言中的 lambda 表达式:

Like HotSpot, Microsoft's CIL-to-machine-code compiler performs dynamic optimization of hot code paths. The .NET source-to-CIL compilers are also explicitly available to programs through the System.CodeDom.Compiler API. A program running on the CLR can directly invoke the compiler to translate C# (or Visual Basic, or other .NET languages) into CIL PE assemblies. These can then be loaded into the running program. As they are loaded, the CLR JIT compiler translates them to machine code. As noted in Sections 3.6.4 and 11.2, C# includes lambda expressions reminiscent of those in functional languages:

Func<int, int> square_func = x => x * x;

Func<int, int> square_func = x => x * x;

这里square_func是一个从整数到整数的函数,它将参数(x)乘以自身,并返回乘积。它类似于 Scheme 中的以下内容:

Here square_func is a function from integers to integers that multiplies its parameter (x) by itself, and returns the product. It is analogous to the following in Scheme:

(让((square-func(lambda(x)(* xx))))…

(let ((square-func (lambda (x) (* x x)))) …

鉴于 C# 声明,我们可以写

Given the C# declaration, we can write

y = square_func(3); // 9

y = square_func(3);     // 9

但是就像 Lisp 允许将函数表示为列表一样,C# 也允许将 lambda 表达式表示为语法树:

But just as Lisp allows a function to be represented as a list, so too does C# allow a lambda expression to be represented as a syntax tree:

表达式<Func<int, int>> square_tree = x => x * x;

Expression<Func<int, int>> square_tree = x => x * x;

现在可以使用库类Expression的各种方法来探索和操作树。 需要时,可以将树转换为 CIL 代码:

Various methods of library class Expression can now be used to explore and manipulate the tree. When desired, the tree can be converted to CIL code:

square_func = square_tree.编译();

square_func = square_tree.Compile();

这些操作大致类似于 Scheme 中的以下内容:

These operations are roughly analogous to the following in Scheme:

(let* ((square-tree '(lambda (x) (* xx))) ;注意引号

(let* ((square-tree '(lambda (x) (* x x))) ; note the quote mark

  (square-func(eval square-tree(scheme-report-environment 5))))

  (square-func (eval square-tree (scheme-report-environment 5))))

 

 

实践上的差异在于,虽然 Scheme 的eval检查lambda表达式的语法有效性并创建动态类型检查所需的元数据,但典型的实现将函数保留为列表(树)形式,并在调用时对其进行解释。C# 的Compile预计会生成 CIL 代码;调用时它将被 JIT 编译并直接执行。■

The difference in practice is that while Scheme's eval checks the syntactic validity of the lambda expression and creates the metadata needed for dynamic type checking, the typical implementation leaves the function in list (tree) form, and interprets it when called. C#'s Compile is expected to produce CIL code; when called it will be JIT compiled and directly executed. ■

例 16.7

Example 16.7

CMU Common Lisp 中的动态编译

Dynamic compilation in CMU Common Lisp

许多 Lisp 方言和实现都采用了解释和编译的明确组合。Common Lisp 包含一个编译函数,该函数以现有(可解释)函数的名称作为参数。作为副作用,它会在该函数上调用编译器,之后该函数(可能)运行得更快:

Many Lisp dialects and implementations have employed an explicit mix of interpretation and compilation. Common Lisp includes a compile function that takes the name of an existing (interpretable) function as argument. As a side effect, it invokes the compiler on that function, after which the function will (presumably) run much faster:

(defun square (x) (* xx));最外层函数声明
(方块 3);9
(编译‘正方形’)
(方块 3);也是 9(但速度更快 :-)

CMU Common Lisp 是该语言的一种广泛使用的开源实现,它包含两个解释器和一个带有两个后端的编译器。所谓的“婴儿”解释器可以理解该语言的一个子集,但可以独立工作。它处理读取-求值-打印循环中的简单表达式,并用于引导系统。“成熟”解释器可以理解整个语言,但需要访问编译器的前端。字节码编译器将源代码转换为类似于 Java 字节码或 CIL 的中间形式。本机代码编译器将其转换为机器代码。通常,除非程序员从命令行或 Common Lisp 程序内部(使用 compile 明确调用编译器,否则程序由“成熟”解释器运行。除非另有指示,否则编译器在调用时会生成本机代码。文档表明编译器的字节码版本的运行速度是本机代码版本的两倍。字节码是可移植的,并且比本机代码密度高 6 倍。它的运行速度比本机代码慢50倍,但比解释器快10倍。■

CMU Common Lisp, a widely used open-source implementation of the language, incorporates two interpreters and a compiler with two back ends. The so-called “baby” interpreter understands a subset of the language, but works stand-alone. It handles simple expressions at the read-eval-print loop, and is used to bootstrap the system. The “grown-up” interpreter understands the whole language, but needs access to the front end of the compiler. The bytecode compiler translates source code to an intermediate form reminiscent of Java bytecode or CIL. The native code compiler translates to machine code. In general, programs are run by the “grown-up” interpreter unless the programmer invokes the compiler explicitly from the command line or from within a Common Lisp program (with compile). The compiler, when invoked, produces native code unless otherwise instructed. Documentation indicates that the bytecode version of the compiler runs twice as fast as the native code version. Bytecode is portable and 6× denser than native code. It runs 50× slower than native code, but 10× faster than the interpreter. ■

例 16.8

Example 16.8

Perl 的编译

Compilation of Perl

与大多数脚本语言一样,Perl 5 将其输入编译为内部语法树格式,然后对其进行解释。在某些情况下,解释器可能需要在执行期间回调到编译器。强制进行这种动态编译的功能包括eval,它编译然后解释字符串;require,它加载库包;以及ee版本的替换命令,它对替换字符串执行表达式求值:

Like most scripting languages, Perl 5 compiles its input to an internal syntax tree format, which it then interprets. In several cases, the interpreter may need to call back into the compiler during execution. Features that force such dynamic compilation include eval, which compiles and then interprets a string; require, which loads a library package; and the ee version of the substitution command, which performs expression evaluation on the replacement string:

$foo = “abc”;
$foo =〜s / b / 2 + 3 / ee;# 将 b 替换为 2 + 3 的值
打印“$foo\n”;# 打印 a5c

还可以通过库调用或perlcc命令行脚本(本身用 Perl 编写)指示 Perl将源代码转换为字节码或机器码。在前一种情况下,输出是一个以#! /usr/bin/perl开头的“可执行”文件(有关#!约定的讨论,请参阅边栏 14.4 )。如果从 shell 调用,此文件将反馈给 Perl 5,Perl 5 将注意到文件的其余部分包含字节码而不是源代码,并将快速重建语法树,以备解释。

Perl can also be directed, via library calls or the perlcc command-line script (itself written in Perl), to translate source code to either bytecode or machine code. In the former case, the output is an “executable” file beginning with #! /usr/bin/perl (see the Sidebar 14.4 for a discussion of the #! convention). If invoked from the shell, this file will feed itself back into Perl 5, which will notice that the rest of the file contains bytecode instead of source, and will perform a quick reconstruction of the syntax tree, ready for interpretation.

如果指示生成机器代码,perlcc会生成一个 C 程序,然后通过 C 编译器运行该程序。C 程序会构建适当的语法树并将其直接传递给 Perl 解释器,从而绕过编译器和字节码到语法树的重建。字节码和机器码后端都被视为实验性的;它们并不适用于所有程序。

If directed to produce machine code, perlcc generates a C program, which it then runs through the C compiler. The C program builds an appropriate syntax tree and passes it directly to the Perl interpreter, bypassing both the compiler and the byte-code-to-syntax-tree reconstruction. Both the bytecode and machine code back ends are considered experimental; they do not work for all programs.

Perl 6 截至 2015 年仍在开发中,旨在进行 JIT 编译。其虚拟机 Parrot 不同寻常之处在于,它提供了一个大型寄存器集,而不是堆栈,用于表达式求值。与 Perl 本身一样(但与 JVM 和 CLR 不同),Parrot 允许在不同的上下文中将变量视为不同类型。目前正在努力将其他脚本语言定位到 Parrot,最终目标是提供与 .NET 语言类似的互操作性。■

Perl 6, still under development as of 2015, is intended to be JIT compiled. Its virtual machine, called Parrot, is unusual in providing a large register set, rather than a stack, for expression evaluation. Like Perl itself—but unlike the JVM and CLR—Parrot allows variables to be treated as different types in different contexts. Work is underway to target other scripting languages to Parrot, with the eventual goal or providing interoperability similar to that of the .NET languages. ■

16-01-9780124104099检查你的理解

Check Your Understanding

11. 什么是即时(JIT) 编译器?与解释或传统编译相比,它的潜在优势是什么?

11. What is a just-in-time (JIT) compiler? What are its potential advantages over interpretation or conventional compilation?

12. 为什么人们更喜欢使用字节码而不是源代码作为 JIT 编译器的输入?

12. Why might one prefer bytecode over source code as the input to a JIT compiler?

13. 动态编译和即时编译有何区别?

13. What distinguishes dynamic compilation from just-in-time compilation?

14. 什么是热路径?它为什么如此重要?

14. What is a hot path? Why is it significant?

15. 在什么情况下 JIT 编译器可以内联扩展虚方法?

15. Under what circumstances can a JIT compiler expand virtual methods in-line?

16. 什么是去优化?什么时候以及为什么需要去优化?

16. What is deoptimization? When and why is it needed?

17. 解释 C# 中 lambda 表达式的函数和表达式树表示之间的区别。

17. Explain the distinction between the function and expression tree representations of a lambda expression in C#.

18. 总结Perl中编译和解释的关系。

18. Summarize the relationship between compilation and interpretation in Perl.

16.2.2 二进制翻译

16.2.2 Binary Translation

即时编译器和动态编译器假设源代码或字节码的可用性,这些代码保留了源代码的所有语义信息。然而,有时重新编译目标代码会很有用。这个过程称为二进制翻译。它允许已编译的程序在具有不同指令集体系结构的机器上运行。一些读者可能还记得Apple 的 Rosetta 系统允许为基于 PowerPC 的旧 Macintosh 计算机编译的程序在基于 x86 的较新 Mac 上运行。Rosetta 建立在大量类似翻译器的经验基础之上。

Just-in-time and dynamic compilers assume the availability of source code or of bytecode that retains all of the semantic information of the source. There are times, however, when it can be useful to recompile object code. This process is known as binary translation. It allows already-compiled programs to be run on a machine with a different instruction set architecture. Some readers may recall Apple's Rosetta system, which allowed programs compiled for older PowerPC-based Macintosh computers to run on newer x86-based Macs. Rosetta built on experience with a long line of similar translators.

二进制翻译的主要挑战是原始源代码到目标代码的翻译中信息的丢失。目标代码通常缺少类型信息以及源代码和字节码中明确界定的子例程和控制流结构。虽然大部分此类信息都出现在编译器的符号表中,有时可能包含在目标文件中以用于调试目的,但供应商通常会在发布商业产品之前将其删除,二进制翻译器无法假设这些信息会存在。

The principal challenge for binary translation is the loss of information in the original source-to-object-code translation. Object code typically lacks both type information and the clearly delineated subroutines and control-flow constructs of source code and bytecode. While most of this information appears in the compiler's symbol table, and may sometimes be included in the object file for debugging purposes, vendors usually delete it before shipping commercial products, and a binary translator cannot assume it will be present.

典型的二进制翻译器会读取目标文件并重建第 15.1.1 节中描述的控制流图。由于缺乏有关基本块的明确信息,这项任务变得复杂。虽然分支(基本块的末尾)很容易识别,但开头却比较困难:由于分支目标有时是在运行时计算的,或者在调度表或虚函数表中查找,所以二进制翻译器必须考虑到控制权有时可能会跳转到“可能的基本”块中间的可能性。由于翻译后的代码通常不会位于与原始代码相同的地址,因此必须将计算出的分支翻译成执行某种表查找的代码,或者依靠解释。

The typical binary translator reads an object file and reconstructs a control flow graph of the sort described in Section 15.1.1. This task is complicated by the lack of explicit information about basic blocks. While branches (the ends of basic blocks) are easy to identify, beginnings are more difficult: since branch targets are sometimes computed at run time or looked up in dispatch tables or virtual function tables, the binary translator must consider the possibility that control may sometimes jump into the middle of a “probably basic” block. Since translated code will generally not lie at the same address as the original code, computed branches must be translated into code that performs some sort of table lookup, or falls back on interpretation.

对于任意目标代码,静态二进制翻译并不总是可行的。除了计算分支之外,问题还包括自修改代码(写入其自身指令空间的程序)、动态生成的代码(例如,如示例 C-9.61 中所述的单指针闭包)以及各种形式的自省,其中程序检查并推理其自身状态(我们将在16.3 节中更全面地考虑这一点)。幸运的是,许多常见的习语可以被识别并作为特殊情况处理,对于无法静态处理的(相对罕见的)情况,二进制翻译器总是可以将某些翻译延迟到运行时,转而使用解释,或者只是通知用户无法进行翻译。在实践中,二进制翻译已被证明非常成功。

Static binary translation is not always possible for arbitrary object code. In addition to computed branches, problems include self-modifying code (programs that write to their own instruction space), dynamically generated code (e.g., for single-pointer closures, as described in Example C-9.61), and various forms of introspection, in which a program examines and reasons about its own state (we will consider this more fully in Section 16.3). Fortunately, many common idioms can be identified and treated as special cases, and for the (comparatively rare) cases that can't be handled statically, a binary translator can always delay some translation until run time, fall back on interpretation, or simply inform the user that translation is not possible. In practice, binary translation has proved remarkably successful.

何时何地进行翻译

Where and When to Translate

例 16.9

Example 16.9

Mac 68K 模拟器

The Mac 68K emulator

大多数二进制翻译器在用户空间中运行,并限制自己使用机器指令集的非特权子集。少数二进制翻译器构建在较低级别。当 Apple 于 1994 年从 Motorola 680x0 处理器转换为 PowerPC 时,他们在操作系统中构建了一个 68K 解释器。次年发布的后续版本为解释器增加了一个基本的二进制翻译器,该翻译器会将经常执行的指令序列缓存在一个小的 (256KB) 缓冲区中。通过将解释器(模拟器)放置在操作系统的最低级别,Apple 能够显著缩短产品上市时间:只有操作系统中性能最关键的部分针对 PowerPC 进行了重写,其余部分则保留为 68K 代码。其他部分则随着时间的推移而重写。■

Most binary translators operate in user space, and limit themselves to the non-privileged subset of the machine's instruction set. A few are built at a lower level. When Apple converted from the Motorola 680x0 processor to the PowerPC in 1994, they built a 68K interpreter into the operating system. A subsequent release the following year augmented the interpreter with a rudimentary binary translator that would cache frequently executed instruction sequences in a small (256KB) buffer. By placing the interpreter (emulator) in the lowest levels of the operating system, Apple was able to significantly reduce its time to market: only the most performance-critical portions of the OS were rewritten for the PowerPC, leaving the rest as 68K code. Additional portions were rewritten over time. ■

例 16.10

Example 16.10

Transmeta Crusoe 处理器

The Transmeta Crusoe processor

20 世纪 90 年代末,Transmeta 公司开发了一种独特的系统,能够通过二进制方式运行未经修改的 x86 操作系统和应用程序翻译。他们的 Crusoe 和 Efficeon 处理器在 2000 年至 2005 年间销售,在宽指令字 ISA(与 Itanium 有远亲关系)之上直接运行专有的“代码变形”软件。该软件与硬件一起设计,可即时将 x86 代码转换为本机代码,并且对于在其上运行的系统而言完全不可见。■

In the late 1990s, Transmeta Corp. developed an unusual system capable of running unmodified x86 operating systems and applications by means of binary translation. Their Crusoe and Efficeon processors, sold from 2000 to 2005, ran proprietary “Code Morphing” software directly on top of a wide-instruction-word ISA (distantly related to the Itanium). This software, designed in conjunction with the hardware, translated x86 code to native code on the fly, and was entirely invisible to systems running above it. ■

例 16.11

Example 16.11

静态二进制翻译

Static binary translation

二进制翻译器在选择翻译内容和翻译时间方面表现出更多的多样性。在最简单的情况下,翻译是一种一次性的离线活动,类似于传统的编译。例如,在 20 世纪 80 年代后期,惠普公司开发了一种二进制翻译器,用于将程序从“经典”HP 3000 系列重新定位到 PA-RISC 处理器。翻译器依赖于操作系统中缺乏动态链接:待翻译程序的所有部分都可以在单个可执行文件中找到。■

Binary translators display even more diversity in their choice of what and when to translate. In the simplest case, translation is a one-time, off-line activity akin to conventional compilation. In the late 1980s, for example, Hewlett Packard Corp. developed a binary translator to retarget programs from their “Classic” HP 3000 line to the PA-RISC processor. The translator depended on the lack of dynamic linking in the operating system: all pieces of the to-be-translated program could be found in a single executable. ■

例 16.12

Example 16.12

动态二进制翻译

Dynamic binary translation

20 世纪 90 年代初,数字设备公司 (DEC) 的计划更为宏大,他们为其新开发的 Alpha 处理器构建了一对翻译器:一个 ( mx ) 用于翻译最初为基于 MIPS 的工作站编译的 Unix 程序,另一个 (VEST) 用于翻译最初为 VAX 编译的 VMS 程序。由于 VMS 支持早期形式的共享库,因此通常不可能静态地识别程序的所有部分。因此,VEST 和mx被设计为“开放式”系统,可以在运行时进行干预,以翻译新加载的库。与 HP 系统一样,DEC 的翻译器将其应用程序的新翻译版本保存到磁盘,以供将来运行使用。■

In a somewhat more ambitious vein, Digital Equipment Corp. (DEC) in the early 1990s constructed a pair of translators for their newly developed Alpha processor: one (mx) to translate Unix programs originally compiled for MIPS-based workstations, the other (VEST) to translate VMS programs originally compiled for the VAX. Because VMS supported an early form of shared libraries, it was not generally possible to statically identify all the pieces of a program. VEST and mx were therefore designed as “open-ended” systems that could intervene, at run time, to translate newly loaded libraries. Like the HP system, DEC's translators saved new, translated versions of their applications to disk, for use in future runs. ■

例 16.13

Example 16.13

混合口译和笔译

Mixed interpretation and translation

在 20 世纪 90 年代中期的后续项目中,DEC 开发了一个在 Alpha 处理器上执行精装 Windows 软件的系统。(微软 Windows NT 操作系统的早期版本适用于 x86 和Alpha 版,但大多数商业软件都只提供 x86 版本。)DEC 的 FX!32 包含二进制翻译器和高度优化的解释器。当用户尝试运行 x86 可执行文件时,FX!32 会首先对其进行解释,收集使用情况统计信息。之后,它会在后台将热代码路径转换为本机代码,并将其存储在数据库中。一旦翻译后的代码可用(在同一执行的稍后阶段或将来的运行期间),解释器就会代替原始代码运行它。

In a subsequent project in the mid-1990s, DEC developed a system to execute shrink-wrapped Windows software on Alpha processors. (Early versions of Microsoft's Windows NT operating system were available for both the x86 and the Alpha, but most commercial software came in x86-only versions.) DEC's FX!32 included both a binary translator and a highly optimized interpreter. When the user tried to run an x86 executable, FX!32 would first interpret it, collecting usage statistics. Later, in the background, it would translate hot code paths to native code, and store them in a database. Once translated code was available (later in the same execution or during future runs), the interpreter would run it in lieu of the original.

设计与实现

Design & Implementation

16.4 仿真和解释

16.4 Emulation and interpretation

虽然解释仿真这两个术语经常一起使用,但这两个概念是截然不同的。正如我们所见,解释是一种语言实现技术:解释器是一种能够执行用待实现语言编写的程序的程序。仿真是一个最终目标:忠实地模仿某些现有系统(通常是处理器或处理器/操作系统对)在其他类型的系统上的行为。仿真器可以使用解释器来执行模拟处理器的指令集。或者,它可以使用二进制翻译、特殊硬件(例如现场可编程门阵列 - FPGA)或这些的组合。

While the terms interpretation and emulation are often used together, the concepts are distinct. Interpretation, as we have seen, is a language implementation technique: an interpreter is a program capable of executing programs written in the to-be-implemented language. Emulation is an end goal: faithfully imitating the behavior of some existing system (typically a processor or processor/OS pair) on some other sort of system. An emulator may use an interpreter to execute the emulated processor's instruction set. Alternatively, it may use binary translation, special hardware (e.g., a field-programmable gate array—FPGA), or some combination of these.

模拟和解释也不同于模拟。模拟器通过捕捉“重要”行为并忽略“不重要”细节来模拟一些复杂系统。例如,气象学家模拟地球的天气系统,但他们并不模拟它们。如果模拟器完全按照原始系统执行操作,则通常认为它是正确的。对于模拟器,需要一些准确性概念:多接近才算足够接近?

Emulation and interpretation are also distinct from simulation. A simulator models some complex system by capturing “important” behavior and ignoring “unimportant” detail. Meteorologists, for example, simulate the Earth's weather systems, but they do not emulate them. An emulator is generally considered correct if it does exactly what the original system does. For a simulator, one needs some notion of accuracy: how close is close enough?

例 16.14

Example 16.14

透明动态翻译

Transparent dynamic translation

现代仿真系统通常采用中间方法。快速、简单的翻译器根据需要创建基本块或子例程的本机版本,并缓存它们以便在给定的执行中重复使用。为了透明(避免修改磁盘上的程序)并适应动态链接,翻译后的代码通常不会从一次执行保留到下一次执行。这种风格的系统包括 Apple 的 Rosetta;HP 的 Aries,它将 PA-RISC 代码重新定位到 Itanium;以及 Intel 的 IA-32 EL,它将 x86 代码重新定位到 Itanium。■

Modern emulation systems typically take an intermediate approach. A fast, simple translator creates native versions of basic blocks or subroutines on demand, and caches them for repeated use within a given execution. For the sake of transparency (to avoid modification of programs on disk), and to accommodate dynamic linking, translated code is usually not retained from one execution to the next. Systems in this style include Apple's Rosetta; HP's Aries, which retargets PA-RISC code to the Itanium; and Intel's IA-32 EL, which retargets x86 code to the Itanium. ■

例 16.15

Example 16.15

翻译和虚拟化

Translation and virtualization

当今使用最广泛的仿真器之一是开源 QEMU(快速仿真)系统。用第 16.1 节的语言来说,QEMU 是一个系统虚拟机。与其他系统 VM 一样,它作为用户级进程运行,模拟裸硬件以在其上运行客户操作系统(以及该系统的工作负载)。与大多数系统 VM 不同,QEMU 可以模拟与底层物理机器截然不同的硬件。为了获得良好的性能,它大量使用二进制翻译,将大量客户系统指令转换为等效的本机指令块。■

Among the most widely used emulators today is the open-source QEMU (quick emulation) system. In the language of Section 16.1, QEMU is a system virtual machine. Like other system VMs, it runs as a user-level process that emulates bare hardware on which to run a guest operating system (and that system's workload). Unlike most system VMs, QEMU can emulate hardware very different from that of the underlying physical machine. For the sake of good performance, it makes heavy use of binary translation, converting large blocks of guest system instructions into equivalent blocks of native instructions. ■

动态优化

Dynamic Optimization

在长期运行的程序中,动态翻译器可能会重新访问热路径并更积极地优化它们。类似的策略也可以应用于不需要翻译的程序,即已经作为底层架构的机器代码存在的程序。据报道,通过利用运行时分析信息,这种动态优化可以使性能比已优化的代码提高 20%。

In a long-running program, a dynamic translator may revisit hot paths and optimize them more aggressively. A similar strategy can also be applied to programs that don't need translation—that is, to programs that already exist as machine code for the underlying architecture. This sort of dynamic optimization has been reported to improve performance by as much as 20% over already-optimized code, by exploiting run-time profiling information.

例 16.16

Example 16.16

Dynamo 动态优化器

The Dynamo dynamic optimizer

动态优化的大部分技术都是由 HP 实验室的 Dynamo 项目在 20 世纪 90 年代末开创的。Dynamo 旨在透明地提高 PA-RISC 指令集应用程序的性能。后续版本 DynamoRIO 是为 x86 编写的。Dynamo 的主要创新是部分执行跟踪的概念:一条热路径,其基本块可以重新组织、优化并缓存为线性序列。

Much of the technology of dynamic optimization was pioneered by the Dynamo project at HP Labs in the late 1990s. Dynamo was designed to transparently enhance the performance of applications for the PA-RISC instruction set. A subsequent version, DynamoRIO, was written for the x86. Dynamo's key innovation was the concept of a partial execution trace: a hot path whose basic blocks can be reorganized, optimized, and cached as a linear sequence.

图 16.3中显示了此类跟踪的一个示例。过程print_matching以集合和谓词为参数,并打印集合中与谓词匹配的所有元素。在运行时,Dynamo 可能会发现该过程经常被调用,并且特定谓词p几乎从不为真。然后,流程图(图左侧)中的热路径可以转换为右侧的跟踪。如果print_matching有时使用不同的谓词p调用,它将使用代码的单独副本。跟踪的分支(在循环终止和谓词检查测试)会跳转到其他跟踪,或者如果尚未创建合适的跟踪,则跳转回 Dynamo。

An example of such a trace appears in Figure 16.3. Procedure print_matching takes a set and a predicate as argument, and prints all elements of the set that match the predicate. At run time, Dynamo may discover that the procedure is frequently called with a particular predicate p that is almost never true. The hot path through the flow graph (left side of the figure) can then be turned into the trace at the right. If print_matching is sometimes called with a different predicate p, it will use a separate copy of the code. Branches out of the trace (in the loop-termination and predicate-checking tests) jump either to other traces or, if appropriate ones have not yet been created, back into Dynamo.

f16-03-9780124104099
图 16.3 创建部分执行跟踪。过程 print_matching(显示在顶部)通常使用特定谓词 p 调用,该谓词通常为假。控制流图(左侧,热块以粗体显示,热路径以灰色显示)可以在运行时重新组织,以改善指令缓存局部性并跨抽象边界进行优化(右侧)。

通过识别和优化跟踪,Dynamo 能够显著提高指令缓存中的局部性,并在单独编译的模块和动态加载的库之间的边界上应用标准代码改进技术。例如,在图 16.3中,它将在print_matchings和谓词p之间联合执行寄存器分配。如果它在跟踪外的分支上插入适当的补偿代码,它甚至可以跨基本块执行指令调度。例如,如果在右侧的分支上放置一份副本,则可以将块测试2中的指令移到循环页脚中。跟踪已被证明是一种非常强大的技术。它们不仅被动态优化器使用,也被动态翻译器(如 Rosetta)和二进制检测工具(如 Pin)使用(将在第16.2.3 节中讨论)。■

By identifying and optimizing traces, Dynamo is able to significantly improve locality in the instruction cache, and to apply standard code improvement techniques across the boundaries between separately compiled modules and dynamically loaded libraries. In Figure 16.3, for example, it will perform register allocation jointly across print_matchings and the predicate p. It can even perform instruction scheduling across basic blocks if it inserts appropriate compensating code on branches out of the trace. An instruction in block test2, for example, can be moved into the loop footer if a copy is placed on the branch to the right. Traces have proved to be a very powerful technique. They are used not only by dynamic optimizers, but by dynamic translators like Rosetta as well, and by binary instrumentation tools like Pin (to be discussed in Section 16.2.3). ■

16.2.3 二进制重写

16.2.3 Binary Rewriting

虽然二进制优化器的目标是在不改变程序行为的情况下提高程序性能,但人们也可以想象出一些旨在改变该行为的工具。二进制重写是一种修改现有可执行程序的通用技术,通常用于插入某种类型的检测。最常见的检测形式是收集性能分析信息。例如,人们可以计算每个子例程被调用的次数,或者每个循环迭代的次数(练习 16.5)。这些计数可以存储在内存的缓冲区中,并在执行结束时转储。或者,人们可以记录所有内存引用。这样的日志通常需要在程序运行时发送到文件中——因为它太长了,内存无法容纳。

While the goal of a binary optimizer is to improve the performance of a program without altering its behavior, one can also imagine tools designed to change that behavior. Binary rewriting is a general technique to modify existing executable programs, typically to insert instrumentation of some kind. The most common form of instrumentation collects profiling information. One might count the number of times that each subroutine is called, for example, or the number of times that each loop iterates (Exercise 16.5). Such counts can be stored in a buffer in memory, and dumped at the end of execution. Alternatively, one might log all memory references. Such a log will generally need to be sent to a file as the program runs—it will be too long to fit in memory.

除了分析之外,二进制重写还可用于

In addition to profiling, binary rewriting can be used to

 模拟新的架构:模拟器感兴趣的操作被跳入特殊运行时库的代码替换(其他代码以本机速度运行)。

 Simulate new architectures: operations of interest to the simulator are replaced with code that jumps into a special run-time library (other code runs at native speed).

 通过识别一系列测试未探索的代码路径来评估测试套件的覆盖率。

 Evaluate the coverage of test suites, by identifying paths through the code that are not explored by a series of tests.

 实现并行程序的模型检查,该过程通过强制程序在不同线程中交错执行操作来暴露竞争条件(示例 13.2 )。

 Implement model checking for parallel programs, a process that exposes race conditions (Example 13.2) by forcing a program through different interleavings of operations in different threads.

 “审计”编译器优化的质量。例如,可以检查加载到寄存器中的值是否始终与已经存在的值相同(此类加载表明编译器可能未能意识到加载是多余的)。

 “Audit” the quality of a compiler's optimizations. For example, one might check whether the value loaded into a register is always the same as the value that was already there (such loads suggest that the compiler may have failed to realize that the load was redundant).

 在缺少动态语义检查的程序中插入动态语义检查。二进制重写不仅可用于简单的检查,如空指针取消引用和算术溢出,还可用于各种内存访问错误,包括未初始化的变量、悬空引用、内存泄漏、“双重删除”(试图释放已释放的内存块)以及访问动态分配数组的末尾。

 Insert dynamic semantic checks into a program that lacks them. Binary rewriting can be used not only for simple checks like null-pointer dereference and arithmetic overflow, but for a wide variety of memory access errors as well, including uninitialized variables, dangling references, memory leaks, “double deletes” (attempts to deallocate an already deallocated block of memory), and access off the ends of dynamically allocated arrays.

更雄心勃勃的是,如侧边栏 16.5 中所述,二进制重写可用于“沙盒化”不受信任的代码,以便它可以安全地在与应用程序其余部分相同的地址空间中执行。

More ambitiously, as described in Sidebar 16.5, binary rewriting can be used to “sandbox” untrusted code so that it can safely be executed in the same address space as the rest of the application.

例 16.17

Example 16.17

ATOM 二进制重写器

The ATOM binary rewriter

重写工具使用的许多技术都是由 Alpha 处理器的 ATOM 二进制重写器开创的。ATOM 是由 DEC 西部研究实验室的研究人员于 20 世纪 90 年代初开发的,是一种静态工具,可以修改程序以供后续执行。

Many of the techniques used by rewriting tools were pioneered by the ATOM binary rewriter for the Alpha processor. Developed by researchers at DEC's Western Research Lab in the early 1990s, ATOM was a static tool that modified a program for subsequent execution.

要使用 ATOM,程序员需要用 C 语言编写仪表分析子程序。在重写过程中,ATOM 会调用仪表程序。通过回调 ATOM,这些程序可以安排重写的应用程序在程序员选择的指令、基本块、子程序或控制流边缘调用分析程序。为了腾出空间对于插入的调用,ATOM 将移动被检测程序的原始指令;为了便于进行这种移动,必须将程序作为一组可重定位的模块提供。对被检测程序没有进行任何其他更改;特别是,数据地址始终保持不变。■

To use ATOM, a programmer would write instrumentation and analysis subroutines in C. Instrumentation routines would be called by ATOM during the rewriting process. By calling back into ATOM, these routines could arrange for the rewritten application to call analysis routines at instructions, basic blocks, subroutines, or control flow edges of the programmer's choosing. To make room for inserted calls, ATOM would move original instructions of the instrumented program; to facilitate such movement, the program had to be provided as a set of relocatable modules. No other changes were made to the instrumented program; in particular, data addresses were always left unchanged. ■

示例系统:Pin 二进制重写器

An Example System: the Pin Binary Rewriter

对于现代处理器而言,ATOM 已被 Pin 取代,Pin 是英特尔研究人员在 21 世纪初开发的二进制重写器,并以开源形式发布。Pin 的设计在很大程度上独立于机器,不仅适用于 x86、x86-64 和 Itanium,还适用于 ARM。

For modern processors, ATOM has been supplanted by Pin, a binary rewriter developed by researchers at Intel in the early 2000s, and distributed as open source. Designed to be largely machine independent, Pin is available not only for the x86, x86-64, and Itanium, but also for ARM.

Pin 直接受到 ATOM 的启发,并具有类似的编程接口。特别是,它保留了检测和分析例程的概念。它还借鉴了 Dynamo 和其他动态翻译工具的思想。最重要的是,它使用 Dynamo 跟踪机制的扩展版本在运行时检测以前未修改的程序;程序在磁盘上的表示永远不会改变。Pin 甚至可以附加已经运行的应用程序,就像我们将在第 16.3.2 节中研究的符号调试器一样。

Pin was directly inspired by ATOM, and has a similar programming interface. In particular, it retains the notions of instrumentation and analysis routines. It also borrows ideas from Dynamo and other dynamic translation tools. Most significantly, it uses an extended version of Dynamo's trace mechanism to instrument previously unmodified programs at run time; the on-disk representation of the program never changes. Pin can even be attached to an already-running application, much like the symbolic debuggers we will study in Section 16.3.2.

与 Dynamo 一样,Pin 首先将基本块的初始跟踪写入运行时跟踪缓存。当它到达无条件分支、预定义的最大条件分支数或预定义的最大指令数时,它会结束跟踪。在写入时,它会在代码中的适当位置插入对分析例程(或短例程的内联版本)的调用。它还维护原始程序地址和跟踪中的地址之间的映射,因此它可以相应地修改特定于地址的指令。一旦它完成创建跟踪,Pin 就会跳转到其第一条指令。退出跟踪的条件分支设置为链接到其他跟踪,或跳回 Pin。

Like Dynamo, Pin begins by writing an initial trace of basic blocks into a runtime trace cache. It ends the trace when it reaches an unconditional branch, a predefined maximum number of conditional branches, or a predefined maximum number of instructions. As it writes, it inserts calls to analysis routines (or in-line versions of short routines) at appropriate places in the code. It also maintains a mapping between original program addresses and addresses in the trace, so it can modify address-specific instructions accordingly. Once it has finished creating a trace, Pin simply jumps to its first instruction. Conditional branches that exit the trace are set to link to other traces, or to jump back into Pin.

间接分支的处理尤为谨慎。基于运行时分析,Pin 维护一组此类分支目标的预测,按最有可能的顺序排列。每个预测都包含原始程序中的地址(用作键)和跟踪缓存中要跳转到的地址。如果所有预测均不匹配,Pin 会在其原始和跟踪缓存地址之间的映射中返回表查找。如果仍未找到匹配项,Pin 会返回指令集解释器,从而使其能够处理动态生成的代码。

Indirect branches are handled with particular care. Based on run-time profiling, Pin maintains a set of predictions for the targets of such branches, sorted most likely first. Each prediction consists of an address in the original program (which serves as a key) and an address to jump to in the trace cache. If none of the predictions match, Pin falls back to table lookup in its mapping between original and trace cache addresses. If match is still not found, Pin falls back on an instruction set interpreter, allowing it to handle even dynamically generated code.

为了减少调用分析例程时保存寄存器的需要,并促进这些例程的内联扩展,Pin 为每个跟踪的指令执行自己的寄存器分配,尽可能对相互链接的跟踪使用类似的分配。在多线程程序中,一个寄存器被静态保留以指向线程特定的缓冲区,必要时寄存器可以溢出。除非之后需要它们的值,否则不会在调用分析例程时保存条件代码。对于可以在基本块内任何地方调用的例程,Pin 会寻找保存和恢复成本最小的位置。

To reduce the need to save registers when calling analysis routines, and to facilitate in-line expansion of those routines, Pin performs its own register allocation for the instructions of each trace, using similar allocations whenever possible for traces that link to one another. In multithreaded programs, one register is statically reserved to point to a thread-specific buffer, where registers can be spilled when necessary. Condition codes are not saved across calls to analysis routines unless their values are needed afterward. For routines that can be called anywhere within a basic block, Pin hunts for a location where the cost of saving and restoring is minimized.

16.2.4 移动代码和沙盒

16.2.4 Mobile Code and Sandboxing

可移植性是后期绑定机器代码的主要动机之一。为一种机器架构或操作系统编译的代码通常不能在另一种机器架构或操作系统上运行。但是,字节码(Java 字节码,CIL)或脚本语言(JavaScript、Visual Basic)中的代码紧凑且独立于机器:它可以轻松地在互联网上移动并在几乎任何平台上运行。这种移动代码越来越普遍。每个主流浏览器都支持 JavaScript;大多数浏览器还允许执行 Java 小程序。Visual Basic 宏通常不仅嵌入在用于使用 Internet Explorer 查看的页面中,还嵌入在通过电子邮件分发的 Excel、Word 和 Outlook 文档中。手机应用程序可以使用移动代码来分发在现有进程中运行的游戏、生产力工具和交互式媒体。

Portability is one of the principal motivations for late binding of machine code. Code that has been compiled for one machine architecture or operating system cannot generally be run on another. Code in a byte code (Java bytecode, CIL) or scripting language (JavaScript, Visual Basic), however, is compact and machine independent: it can easily be moved over the Internet and run on almost any platform. Such mobile code is increasingly common. Every major browser supports JavaScript; most enable the execution of Java applets as well. Visual Basic macros are commonly embedded not only in pages meant for viewing with Internet Explorer, but also in Excel, Word, and Outlook documents distributed via email. Cell phone apps may use mobile code to distribute games, productivity tools, and interactive media that run within an existing process.

从某种意义上说,移动代码并不是什么新鲜事:我们几乎所有的软件都来自其他来源;我们通过互联网下载软件,或者从 DVD 上安装软件。从历史上看,这种使用模式依赖于信任(我们假设知名公司的软件是安全的)以及安装的明确性和偶然性。近年来,情况发生了变化,人们希望频繁下载代码,从可能不受信任的来源下载,而且通常用户没有意识到。

In some sense, mobile code is nothing new: almost all our software comes from other sources; we download it over the Internet or perhaps install it from a DVD. Historically, this usage model has relied on trust (we assume that software from a well-known company will be safe) and on the very explicit and occasional nature of installation. What has changed in recent years is the desire to download code frequently, from potentially untrusted sources, and often without the conscious awareness of the user.

移动代码具有多种风险。它可能会访问和泄露机密信息(间谍软件)。它可能会以令人讨厌的方式干扰计算机的正常使用(广告软件)。它可能会损坏现有程序或数据,或保存自身副本并在用户不知情的情况下运行(各种恶意软件)。特别是在严重的情况下,它可能会把主机当作“僵尸”来对其他用户发起攻击。

Mobile code carries a variety of risks. It may access and reveal confidential information (spyware). It may interfere with normal use of the computer in annoying ways (adware). It may damage existing programs or data, or save copies of itself that run without the user's intent (malware of various kinds). In particular egregious cases, it may use the host machine as a “zombie” from which to launch attacks on other users.

设计与实现

Design & Implementation

16.5 通过二进制重写创建沙盒

16.5 Creating a sandbox via binary rewriting

二进制重写提供了一种实现沙箱的有吸引力的方法。虽然一般来说没有办法确保代码按照预期执行(即使是自己的代码也很少能确定这一点),但二进制重写器可以

Binary rewriting provides an attractive means to implement a sandbox. While there is in general no way to ensure that code does what it is supposed to do (one is seldom sure of that even with one's own code), a binary rewriter can

 验证每个加载和存储的地址,以确保不受信任的代码仅访问其自己的数据,并避免对齐错误

 Verify the address of every load and store, to make sure untrusted code accesses only its own data, and to avoid alignment faults

 类似地验证每个分支和调用,以防止控制权通过除返回之外的任何方式离开沙箱

 Similarly verify every branch and call, to prevent control from leaving the sandbox by any means other than returning

 验证所有操作码,防止非法指令错误

 Verify all opcodes, to prevent illegal instruction faults

 仔细检查任何可能产生错误的算术指令的参数

 Double-check the parameters to any arithmetic instruction that may generate a fault

 审计(或禁止)所有系统调用

 Audit (or forbid) all system calls

 使用向后跳转来限制不受信任的代码的运行时间(特别是为了防止任何无限循环)

 Instrument backward jumps to limit the amount of time that untrusted code can run (and in particular to preclude any infinite loops)

为了防止意外和恶意行为,必须在某种沙盒中执行移动代码,如边栏 14.6 中所述。沙盒的创建很困难,因为必须保护的资源种类繁多。至少,需要监视或限制对处理器周期、代码自身指令和数据之外的内存、文件系统、网络接口、其他设备(例如,密码可能通过窥探键盘而被盗)、窗口系统(例如,禁用弹出广告)以及操作系统提供的任何其他潜在危险服务的访问。

To protect against unwanted behavior, both accidental and malicious, mobile code must be executed in some sort of sandbox, as described in Sidebar 14.6. Sandbox creation is difficult because of the variety of resources that must be protected. At a minimum, one needs to monitor or limit access to processor cycles, memory outside the code's own instructions and data, the file system, network interfaces, other devices (passwords, for example, may be stolen by snooping the keyboard), the window system (e.g., to disable pop-up ads), and any other potentially dangerous services provided by the operating system.

沙盒机制位于语言实现和操作系统之间的边界。传统上,操作系统提供的虚拟内存技术可用于限制对内存的访问,但对于许多形式的移动代码而言,这通常过于昂贵。当今最常见的两种技术(均依赖于本章讨论的技术)是二进制重写和在非信任解释器中执行。这两种情况都因安全性和实用性之间的内在矛盾而变得复杂:我们允许非信任代码执行的操作越少,它的用处就越小。没有一种策略可能适用于所有情况。如果小程序只能操作窗口中的图像,那么它们可能是完全安全的,但嵌入在电子表格中的宏可能无法在不更改用户数据的情况下完成其工作。未来工作的主要挑战是找到一种方法来帮助用户(他们不能理解技术细节)做出明智的决定,决定在移动代码中允许什么和不允许什么。

Sandboxing mechanisms lie at the boundary between language implementation and operating systems. Traditionally, OS-provided virtual memory techniques might be used to limit access to memory, but this is generally too expensive for many forms of mobile code. The two most common techniques today—both of which rely on technology discussed in this chapter—are binary rewriting and execution in an untrusting interpreter. Both cases are complicated by an inherent tension between safety and utility: the less we allow untrusted code to do, the less useful it can be. No single policy is likely to work in all cases. Applets may be entirely safe if all they can do is manipulate the image in a window, but macros embedded in a spreadsheet may not be able to do their job without changing the user's data. A major challenge for future work is to find a way to help users— who cannot be expected to understand the technical details—to make informed decisions about what and what not to allow in mobile code.

16-01-9780124104099检查你的理解

Check Your Understanding

19. 什么是二进制翻译?什么时候以及为什么需要它?

19. What is binary translation? When and why is it needed?

20. 解释静态和动态二进制翻译之间的权衡。

20. Explain the tradeoffs between static and dynamic binary translation.

21. 什么是仿真?它与解释和模拟有何关系?

21. What is emulation? How is it related to interpretation and simulation?

22. 什么是动态优化?它如何改进静态优化?

22. What is dynamic optimization?. How can it improve on static optimization?

23. 什么是二进制重写?它与二进制翻译和动态优化有何不同?

23. What is binary rewriting? How does it differ from binary translation and dynamic optimization?

24.描述 部分执行跟踪的概念。它对动态优化和重写为什么重要?

24. Describe the notion of a partial execution trace. Why is it important to dynamic optimization and rewriting?

25. 什么是移动代码

25. What is mobile code?

26. 什么是沙盒?什么时候需要它?为什么需要它?如何实现它?

26. What is sandboxing? When and why is it needed? How can it be implemented?

16.3 检查/自省

16.3 Inspection/Introspection

符号表元数据使实用程序(即时和动态编译器、优化器、调试器、分析器和二进制重写器)可以轻松检查程序并推断其结构和类型。我们将在第 16.3.216.3.3节中特别考虑调试器和分析器。但是,没有理由将元数据的使用仅限于外部工具,事实上也并非如此:Lisp 长期以来一直允许程序推断其自身的内部结构和类型(这种推理有时称为自省)。Java 和 C# 通过反射API提供类似的功能,该 API 允许程序仔细阅读其自己的元数据。反射也出现在其他几种语言中,包括 Prolog(侧边栏 12.2)和所有主要的脚本语言。在动态类型语言(如 Lisp)中,反射是必不可少的:它允许库或应用程序函数对其自己的参数进行类型检查。在静态类型语言中,反射支持各种传统上不可行的编程习惯用法。

Symbol table metadata makes it easy for utility programs—just-in-time and dynamic compilers, optimizers, debuggers, profilers, and binary rewriters—to inspect a program and reason about its structure and types. We consider debuggers and profilers in particular in Sections 16.3.2 and 16.3.3. There is no reason, however, why the use of metadata should be limited to outside tools, and indeed it is not: Lisp has long allowed a program to reason about its own internal structure and types (this sort of reasoning is sometimes called introspection). Java and C# provide similar functionality through a reflection API that allows a program to peruse its own metadata. Reflection appears in several other languages as well, including Prolog (Sidebar 12.2) and all the major scripting languages. In a dynamically typed language such as Lisp, reflection is essential: it allows a library or application function to type check its own arguments. In a statically typed language, reflection supports a variety of programming idioms that were not traditionally feasible.

16.3.1 反射

16.3.1 Reflection

例 16.18

Example 16.18

查找引用变量的具体类型

Finding the concrete type of a reference variable

显然,反射在打印诊断信息时很有用。假设我们尝试调试 Java 中的旧式(非泛型)队列,并且想要跟踪在其中移动的对象。在dequeue方法中,在返回Object类型的对象rtn之前,我们可能会写

Trivially, reflection can be useful when printing diagnostics. Suppose we are trying to debug an old-style (nongeneric) queue in Java, and we want to trace the objects that move through it. In the dequeue method, just before returning an object rtn of type Object, we might write

System.out.println(“出队 a “ + rtn.getClass().getName());

System.out.println(“Dequeued a “ + rtn.getClass().getName());

如果出队对象是一个装箱的整数,我们将看到

If the dequeued object is a boxed int, we will see

出队 java.lang.Integer

Dequeued a java.lang.Integer

更重要的是,反射在操纵其他程序的程序中很有用。例如,大多数程序开发环境都有组织和“漂亮地打印”程序的类、方法和变量的机制。在具有反射的语言中,这些工具无需检查源代码:如果它们将已编译的程序加载到自己的地址空间中,它们可以使用反射 API 来查询编译器创建的符号表信息。解释器、调试器和分析器可以以类似的方式工作。在分布式系统中,程序可以使用反射来创建通用的序列化机制,能够将几乎任意的结构转换为可以通过网络发送并在另一端重新组装的线性字节流。(Java 和 C# 都在其标准库中包含此类机制,这些机制是在基本语言之上实现的。)在日益动态的在互联网应用世界中,人们甚至可以创建约定,让程序“查询”新发现的对象,看它实现了哪些方法,然后选择调用其中的方法。

More significantly, reflection is useful in programs that manipulate other programs. Most program development environments, for example, have mechanisms to organize and “pretty-print” the classes, methods, and variables of a program. In a language with reflection, these tools have no need to examine source code: if they load the already-compiled program into their own address space, they can use the reflection API to query the symbol table information created by the compiler. Interpreters, debuggers, and profilers can work in a similar fashion. In a distributed system, a program can use reflection to create a general-purpose serialization mechanism, capable of transforming an almost arbitrary structure into a linear stream of bytes that can be sent over a network and reassembled at the other end. (Both Java and C# include such mechanisms in their standard library, implemented on top of the basic language.) In the increasingly dynamic world of Internet applications, one can even create conventions by which a program can “query” a newly discovered object to see what methods it implements, and then choose which of these to call.

当然,不加约束地使用反射会带来危险。由于反射允许应用程序窥视类的实现(例如,列出其私有成员),因此它违反了抽象和信息隐藏的正常规则。某些安全策略可能会禁用它(例如,在沙盒环境中)。通过限制目标代码与源代码的差异程度,它可能会阻止某些形式的代码改进。

There are dangers, of course, associated with the undisciplined use of reflection. Because it allows an application to peek inside the implementation of a class (e.g., to list its private members), reflection violates the normal rules of abstraction and information hiding. It may be disabled by some security policies (e.g., in sandboxed environments). By limiting the extent to which target code can differ from the source, it may preclude certain forms of code improvement.

例 16.19

Example 16.19

反射不应该做什么

What not to do with reflection

至少对于面向对象语言来说,反射最常见的陷阱可能是倾向于编写由类型信息驱动的 case(switch)语句:

Perhaps the most common pitfall of reflection, at least for object-oriented languages, is the temptation to write case (switch) statements driven by type information:

程序旋转(s:形状)

procedure rotate(s : shape)

 case shape.type        在 Java 中不要这样做!

 case shape.type of        don't do this in Java!

  正方形:rotate_square(s)

  square: rotate_square(s)

  三角形:旋转三角形

  triangle: rotate_triangle(s)

  圆圈:         ——无操作

  circle:         –– no-op

虽然这种代码在 Lisp 中很常见(并且很合适),但在面向对象语言中,使用子类型多态性来编写会更好:

While this kind of code is common (and appropriate) in Lisp, in an object-oriented language it is much better written with subtype polymorphism:

s.rotate()          ––虚拟方法调用

s.rotate()         ––virtual method call

Java 反射

Java Reflection

例 16.20

Example 16.20

Java 类命名约定

Java class-naming conventions

Java 的根类Object支持getClass方法,该方法返回java.lang.Class的实例。此类的对象又支持大量反射操作,其中包括我们在示例 16.18中使用的getName方法。对getName的调用将返回类的完全限定名称,因为它嵌入在包层次结构中。对于数组类型,命名约定取自

Java's root class, Object, supports a getClass method that returns an instance of java.lang.Class. Objects of this class in turn support a large number of reflection operations, among them the getName method we used in Example 16.18. A call to getName returns the fully qualified name of the class, as it is embedded in the package hierarchy. For array types, naming conventions are taken from the

int[] A = 新 int[10];
系统.out.println(A.getClass().getName());// 打印“[I”
字符串[] C = 新字符串[10];
系统.out.println(C.getClass().getName());//“[Ljava.lang.String;”
Foo[][] D = 新 Foo[10][10];
系统.out.println(D.getClass().getName());//“[[LFoo;”

这里Foo被假定为默认(最外层)包中的用户定义类。左方括号表示数组类型;它后面跟着数组的元素类型。内置类型(例如int)在此上下文中用单字母名称(例如I )表示。用户定义类型用L表示,后跟完全限定的类名,以分号结尾。请注意第二个示例( C )与图 16.1中常量池中的条目 #12的相似性:该条目给出main的参数类型(括号中)和返回类型(V表示void)。每个 Java 程序员都知道,main需要一个字符串数组。■

Here Foo is assumed to be a user-defined class in the default (outermost) package. A left square bracket indicates an array type; it is followed by the array's element type. The built-in types (e.g., int) are represented in this context by single-letter names (e.g., I). User-defined types are indicated by an L, followed by the fully qualified class name and terminated by a semicolon. Notice the similarity of the second example (C) to entry #12 in the constant pool of Figure 16.1: that entry gives the parameter types (in parentheses) and return type (V means void) of main. As every Java programmer knows, main expects an array of strings. ■

例 16.21

Example 16.21

获取特定类的信息

Getting information on a particular class

调用o.getClass()会返回o所引用对象的具体类型信息,而不是引用o的抽象类型信息。如果我们想要某个特定类型的Class对象,我们可以创建该类型的虚拟对象:

A call to o.getClass() returns information on the concrete type of the object referred to by o, not on the abstract type of the reference o. If we want a Class object for a particular type, we can create a dummy object of that type:

对象o = 新对象();

Object o = new Object();

System.out.println(o.getClass().getName());       //“java.lang.Object”

System.out.println(o.getClass().getName());      // “java.lang.Object”

或者,我们可以将伪字段名称.class附加到类型本身的名称之后:

Alternatively, we can append the pseudo field name .class to the name of the type itself:

System.out.println(Object.class.getName());       //“java.lang.Object”

System.out.println(Object.class.getName());      // “java.lang.Object”

反向来说,我们可以使用Class的静态方法forName来获取具有给定(完全限定)字符串名称的类型的Class对象:

In the reverse direction, we can use static method forName of class Class to obtain a Class object for a type with a given (fully qualified) character string name:

类stringClass = Class.forName(“java.lang.String”);

Class stringClass = Class.forName(“java.lang.String”);

类intArrayClass = Class.forName(“[I”);

Class intArrayClass = Class.forName(“[I”);

forName方法仅适用于引用类型。对于内置函数,可以使用.class语法或标准包装类之一的.TYPE字段:

Method forName works only for reference types. For built-ins, one can either use the .class syntax or the .TYPE field of one of the standard wrapper classes:

类 intClass = Integer.TYPE;

Class intClass = Integer.TYPE;

给定一个Class对象c,可以调用c.getSuperclass()来获取c的父类的Class对象。类似地,c.getClasses()将返回一个Class对象数组,每个对象对应于c的类中声明的每个公共类。也许更有趣的是,c.getMethods()c.getFields()c.getConstructors()将返回表示所有c的公共方法、字段和构造函数(包括从祖先类继承的方法、字段和构造函数)的对象数组。这些数组的元素分别是Method、FieldConstructor类的实例。它们在包java.lang.reflect中声明,作用类似于 Class 这些类的许多方法允许查询 Java 类型系统的几乎任何方面,包括修饰符(static 、 private 、 final 、 abstract等)、泛型的类型参数(但不是泛型实例的类型参数 - 这些参数已被删除)、类实现的接口、方法抛出的异常等等。最引人注目的、无法通过 Java 反射 API 获取的东西可能是实现方法的字节码。然而,即使是这个,也可以使用第三方工具进行检查,例如 Apache 字节码工程库 (BCEL) 或 ObjectWeb 的 ASM,这两个工具都是开源的。

Given a Class object c, one can call c.getSuperclass() to obtain a Class object for c's parent. In a similar vein, c.getClasses() will return an array of Class objects, one for each public class declared within c's class. Perhaps more interesting, c.getMethods(), c.getFields(), and c.getConstructors() will return arrays of objects representing all c's public methods, fields, and constructors (including those inherited from ancestor classes). The elements of these arrays are instances of classes Method, Field, and Constructor, respectively. These are declared in package java.lang.reflect, and serve roles analogous to that of Class. The many methods of these classes allow one to query almost any aspect of the Java type system, including modifiers (static, private, final, abstract, etc.), type parameters of generics (but not of generic instances—those are erased), interfaces implemented by classes, exceptions thrown by methods, and much more. Perhaps the most conspicuous thing that is not available through the Java reflection API is the bytecode that implements methods. Even this, however, can be examined using third-party tools such as the Apache Byte Code Engineering Library (BCEL) or ObjectWeb's ASM, both of which are open source.

例 16.22

Example 16.22

列出 Java 类的方法

Listing the methods of a Java class

图 16.4显示了 Java 代码,用于列出给定类中声明的方法(但未被继承)。还显示了AccessibleObject的输出,它是Method、FieldConstructor的父类。(此类的主要目的是控制反射接口是否可以用来覆盖给定对象的访问控制[ private , protected ]。)■

Figure 16.4 shows Java code to list the methods declared in (but not inherited by) a given class. Also shown is output for AccessibleObject, the parent class of Method, Field, and Constructor. (The primary purpose of this class is to control whether the reflection interface can be used to override access control [private, protected] for the given object.) ■

f16-04-9780124104099
图 16.4 Java 反射代码列出给定类的方法。示例输出如下所示。

例 16.23

Example 16.23

使用反射调用方法

Calling a method with reflection

甚至可以使用反射来调用编译时未知类的对象的方法。假设有人创建了一个包含单个整数的堆栈:

One can even use reflection to call a method of an object whose class is not known at compile time. Suppose that someone has created a stack containing a single integer:

堆栈 s = 新堆栈 ();

Stack s = new Stack();

s.推送(新整数(3));

s.push(new Integer(3));

现在假设我们传递了这个堆栈作为Object类型的参数u 。我们可以使用反射来探索u的具体类型。在此过程中,我们会发现它的第二个方法名为pop,不接受任何参数并返回一个Object结果。我们可以使用Method.invoke调用此方法:

Now suppose we are passed this stack as a parameter u of Object type. We can use reflection to explore the concrete type of u. In the process we will discover that its second method, named pop, takes no arguments and returns an Object result. We can call this method using Method.invoke:

方法 uMethods[] = u.getClass().getMethods();

Method uMethods[] = u.getClass().getMethods();

方法 method1 = uMethods[1];

Method method1 = uMethods[1];

对象 rtn = 方法1.invoke(u);       // u.pop()

Object rtn = method1.invoke(u);      // u.pop()

调用rtn.getClass().getName()将返回java.lang.Integer。调用((Integer) rtn).intValue()将返回最初推送到s 的值 3 。■

A call to rtn.getClass().getName() will return java.lang.Integer. A call to ((Integer) rtn).intValue() will return the value 3 that was originally pushed into s. ■

其他语言

Other Languages

C# 的反射 API 与 Java 的类似:System.Type类似于java.lang.Class;System.Reflection类似于java.lang.reflect。伪函数typeof充当 Java 的伪字段.class的角色。更实质性的差异源于 PE 程序集包含的信息比 Java 类文件中的信息多一点。例如,我们可以在 C# 中请求形式参数的名称,而不仅仅是它们的类型。更重要的是,对泛型使用具体化而不是擦除意味着我们可以检索用于实例化给定对象的类型参数的精确信息。也许最大的区别是 .NET 提供了一个标准库System.Reflection.Emit创建 PE 程序集并使用 CIL 填充它们。Reflection.Emit 的功能大致相当于上一小节中提到的 BCEL 和 ASM 工具的功能。但是,由于它是标准库的一部分,因此它可用于在 CLI 上运行的任何程序。

C#'s reflection API is similar to that of Java: System.Type is analogous to java.lang.Class; System.Reflection is analogous to java.lang.reflect. The pseudo function typeof plays the role of Java's pseudo field .class. More substantive differences stem from the fact that PE assemblies contain a bit more information than is found in Java class files. We can ask for names of formal parameters in C#, for example, not just their types. More significantly, the use of reification instead of erasure for generics means that we can retrieve precise information on the type parameters used to instantiate a given object. Perhaps the biggest difference is that .NET provides a standard library, System.Reflection.Emit, to create PE assemblies and to populate them with CIL. The functionality of Reflection.Emit is roughly comparable to that of the BCEL and ASM tools mentioned in the previous subsection. Because it is part of the standard library, however, it is available to any program running on the CLI.

例 16.24

Example 16.24

Ruby 中的反射功能

Reflection facilities in Ruby

所有主流脚本语言(Perl、PHP、Tcl、Python、Ruby、JavaScript)都提供了广泛的反射机制。具体功能集因语言而异,语法也有很大差异,但都允许程序探索自己的结构和类型。从程序员的角度来看,Java 和 C# 中的反射与脚本语言中的反射之间的主要区别在于,脚本语言(如 Lisp)是动态类型的。例如,在 Ruby 中,我们可以发现对象的类、类或对象的方法以及每个方法所需的参数数量,但参数本身在调用方法之前是无类型的。在下面的代码中,方法p将其参数打印到标准输出,后跟换行符:

All of the major scripting languages (Perl, PHP, Tcl, Python, Ruby, JavaScript) provide extensive reflection mechanisms. The precise set of capabilities varies some from language to language, and the syntax varies quite a bit, but all allow a program to explore its own structure and types. From the programmer's point of view, the principal difference between reflection in Java and C# on the one hand, and in scripting languages on the other, is that the scripting languages—like Lisp—are dynamically typed. In Ruby, for example, we can discover the class of an object, the methods of a class or object, and the number of parameters expected by each method, but the parameters themselves are untyped until the method is called. In the following code, method p prints its argument to standard output, followed by a newline:

正方形 = {2=>4, 3=>9}
p 正方形.类# 哈希
p Hash.public_instance_methods.长度#146——哈希有很多方法
p 正方形.public_methods.长度#146——同样的方法
m = Hash.public_instance_methods[12]
下午# “:店铺”
p 平方.方法(m).元数# 2 – 要存储的键和值

与 Java 和 C# 一样,我们也可以调用编译时未知名称的方法:

As in Java and C#, we can also invoke a method whose name was not known at compile time:

正方形. 存储(1,1)# 静态调用
p 平方#{2=>4, 3=>9, 1=>1}
正方形.发送(m,0,0)# 动态调用
p 平方#{2=>4, 3=>9, 1=>1, 0=>0}

正如本节开头所建议的,脚本语言(以及 Lisp 和 Prolog)中的反射在某种意义上比 Java 或 C# 中的反射更“自然”:运行时需要详细的符号表信息来执行动态类型检查;在解释实现中,它也随时可用。Lisp 程序员几十年来都知道反射可用于许多其他目的。Java 和 C# 的设计者显然认为这些目的很有价值,足以证明将反射(实现复杂性相当高)添加到具有静态类型的编译语言中是合理的。

As suggested at the beginning of this section, reflection is in some sense more “natural” in scripting languages (and in Lisp and Prolog) than it is in Java or C#: detailed symbol table information is needed at run time to perform dynamic type checks; in an interpreted implementation, it is also readily available. Lisp programmers have known for decades that reflection was useful for many additional purposes. The designers of Java and C# clearly felt these purposes were valuable enough to justify adding reflection (with considerably higher implementation complexity) to a compiled language with static typing.

注释和属性

Annotations and Attributes

Java 和 C# 都允许程序员扩展编译器保存的元数据。在 Java 中,这些扩展采用附加在声明上的注解的形式。编程语言中内置了几种注解。它们起着编译指示的作用。例如,在示例 C-7.65 中,我们注意到,当将泛型类赋值给等效非泛型类的变量时,Java 编译器将生成警告。警告表示代码不是静态类型安全的,运行时可能会出现错误消息。如果程序员确信不会出现错误,则可以通过在出现赋值的方法前加上注解@SuppressWarnings(“unchecked”)来禁用编译时警告。

Both Java and C# allow the programmer to extend the metadata saved by the compiler. In Java, these extensions take the form of annotations attached to declarations. Several annotations are built into the programming language. These play the role of pragmas. In Example C-7.65, for example, we noted that the Java compiler will generate warnings when a generic class is assigned into a variable of the equivalent nongeneric class. The warning indicates that the code is not statically type-safe, and that an error message is possible at run time. If the programmer is certain that the error cannot arise, the compile-time warning can be disabled by prefixing the method in which the assignment appears with the annotation @SuppressWarnings(“unchecked”).

例 16.25

Example 16.25

Java 中的用户定义注释

User-defined annotations in Java

通常,Java 注释类似于接口,其方法不接受任何参数,不抛出任何异常,并返回有限数量的预定义类型之一的值。图 16.5显示了用户定义注释的示例。如果我们运行该程序,它将打印

In general, a Java annotation resembles an interface whose methods take no parameters, throw no exceptions, and return values of one of a limited number of predefined types. An example of a user-defined annotation appears in Figure 16.5. If we run the program it will print

f16-05-9780124104099
图 16.5 Java 中的用户定义注解。保留是注解的内置注解。它在这里表示文档注解应该保存在 Java 编译器生成的类文件中,在那里它们将可用于运行时反射。
作者:迈克尔·斯科特
日期:2015 年 7 月
修订:0.1
文档字符串:说明注释的使用

例 16.26

Example 16.26

C# 中的用户定义注释

User-defined annotations in C#

图 16.5的 C# 等效代码如图16.6所示。此处用户定义的批注(在 C# 中称为属性)是类,而不是接口,将属性附加到声明的语法使用方括号而不是@符号。■

The C# equivalent of Figure 16.5 appears in Figure 16.6. Here user-defined annotations (known as attributes in C#) are classes, not interfaces, and the syntax for attaching an attribute to a declaration uses square brackets instead of an @ sign. ■

f16-06-9780124104099
图 16.6 C# 中的用户定义属性。此代码大致相当于图 16.5中的 Java 版本。AttributeUsage 是一个预定义属性,指示其附加到其声明的属性的性质。

实际上,注释(属性)充当编译器支持的注释,具有明确定义的结构和 API,可让它们自动读取。正如我们所见,它们可以由编译器(作为指令)或反射程序读取。它们也可以由独立工具读取。这些工具的用途非常广泛。

In effect, annotations (attributes) serve as compiler-supported comments, with well-defined structure and an API that makes them accessible to automated perusal. As we have seen, they may be read by the compiler (as pragmas) or by reflective programs. They may also be read by independent tools. Such tools can be surprisingly versatile.

例 16.27

Example 16.27

javadoc

javadoc

一个明显的用途是自动创建文档。Java 注释(在 Java 5 中首次引入)至少部分地受到了早期的 javadoc 工具,它基于 Java 源代码中的结构化注释生成 HTML 格式的文档。当@Documented注释附加到用户定义注释的声明时,表示javadoc在创建报告时应包含该注释。人们很容易想象出更复杂的文档系统,它可以跟踪程序的版本历史和错误报告。■

An obvious use is the automated creation of documentation. Java annotations (first introduced in Java 5) were inspired at least in part by experience with the earlier javadoc tool, which produces HTML-formatted documentation based on structured comments in Java source code. The @Documented annotation, when attached to the declaration of a user-defined annotation, indicates that javadoc should include the annotation when creating its reports. One can easily imagine more sophisticated documentation systems that tracked the version history and bug reports for a program over time. ■

例 16.28

Example 16.28

组件间通信

Intercomponent communication

.NET 中的各种通信技术广泛使用属性来指示哪些方法可用于远程执行、它们的参数应如何编组为消息、哪些类需要序列化代码等等。自动工具使用这些属性来创建适当的远程通信存根,如第 C-13.5.4 节中所述(以语言中立的术语)。■

The various communication technologies in .NET make extensive use of attributes to indicate which methods should be available for remote execution, how their parameters should be marshalled into messages, which classes need serialization code, and so forth. Automatic tools use these attributes to create appropriate stubs for remote communication, as described (in language-neutral terms) in Section C-13.5.4. ■

例 16.29

Example 16.29

LINQ 的属性

Attributes for LINQ

类似地,.NET LINQ 机制使用属性来定义用户程序中的类和关系数据库中的表之间的映射,从而允许一个自动生成SQL 查询的工具,用于实现迭代器和其他语言级操作。■

In a similar vein, the .NET LINQ mechanism uses attributes to define the mapping between classes in a user program and tables in a relational database, allowing an automatic tool to generate SQL queries that implement iterators and other language-level operations. ■

例 16.30

Example 16.30

Java 建模语言

The Java Modeling Language

更宏伟的目标是,可以使用独立工具来修改或分析基于注释的程序。可以想象将日志代码插入某些带注释的方法中,或者构建一个测试工具,该工具使用指定​​的参数调用带注释的方法并检查预期结果(练习 16.11)。JML(Java 建模语言)允许程序员为类、方法和语句指定先决条件、后置条件和不变量,就像我们在第4.1 节“断言”中考虑的那样。JML 建立在早期多语言、多机构项目 Larch 的经验之上。与javadoc一样,JML 使用结构化注释而不是较新的编译器支持的注释来表达其规范,因此它们不会自动包含在类文件中。可以使用各种工具,但是,验证程序是否符合其规范,可以通过静态方式(在可能的情况下)或在运行时(通过插入语义检查)进行。■

In an even more ambitious vein, independent tools can be used to modify or analyze programs based on annotations. One could imagine inserting logging code into certain annotated methods, or building a testing harness that called annotated methods with specified arguments and checked for expected results (Exercise 16.11). JML, the Java Modeling Language, allows the programmer to specify preconditions, post-conditions, and invariants for classes, methods, and statements, much like those we considered under “Assertions” in Section 4.1. JML builds on experience with an earlier multilanguage, multi-institution project known as Larch. Like javadoc, JML uses structured comments rather than the newer compiler-supported annotations to express its specifications, so they are not automatically included in class files. A variety of tools can be used, however, to verify that a program conforms to its specifications, either statically (where possible) or at run time (via insertion of semantic checks). ■

例 16.31

Example 16.31

Java 注释处理器

Java annotation processors

Java 5 引入了一个名为 apt 的程序,旨在促进注释处理工具的构建。该工具的功能随后被集成到 Sun 的 Java 6 编译器中。它的关键支持功能是一组 API(在javax.annotation.processing中),允许将注释处理器类添加到程序中,以便编译器在编译时运行它。使用反射,类可以仔细阅读程序的静态结构(包括注释和有关泛型的完整信息),其方式与传统反射机制允许正在运行的程序仔细阅读其自己的类型和结构的方式非常相似。■

Java 5 introduced a program called apt designed to facilitate the construction of annotation processing tools. The functionality of this tool was subsequently integrated into Sun's Java 6 compiler. Its key enabling feature is a set of APIs (in javax.annotation.processing) that allow an annotation processor class to be added to a program in such a way that the compiler will run it at compile time. Using reflection, the class can peruse the static structure of the program (including annotations and full information on generics) in much the same way that traditional reflection mechanisms allow a running program to peruse its own types and structure. ■

16.3.2 符号调试

16.3.2 Symbolic Debugging

大多数程序员都熟悉符号调试器:它们内置于大多数编程语言解释器、虚拟机和集成程序开发环境中。它们也可以作为独立工具使用,其中最著名的可能是 GNU 的gdb。形容词符号是指调试器对高级语言语法(原始程序中的符号)的理解。早期的调试器只理解汇编语言。

Most programmers are familiar with symbolic debuggers: they are built into most programming language interpreters, virtual machines, and integrated program development environments. They are also available as stand-alone tools, of which the best known is probably GNU's gdb. The adjective symbolic refers to a debugger's understanding of high-level language syntax—the symbols in the original program. Early debuggers understood assembly language only.

在典型的调试会话中,用户在调试器的控制下启动程序,或将调试器附加到已经运行的程序。然后,调试器允许用户执行两种主要操作。一种检查或修改程序数据;另一种控制执行:启动、停止、单步执行以及建立断点观察点。断点指定如果到达源代码中的特定位置则应停止执行。观察点指定如果读取或写入特定变量则应停止执行。断点和观察点通常都可以设为有条件的,这样只有当特定的布尔谓词计算为真时,执行才会停止。

In a typical debugging session, the user starts a program under the control of the debugger, or attaches the debugger to an already running program. The debugger then allows the user to perform two main kinds of operations. One kind inspects or modifies program data; the other controls execution: starting, stopping, stepping, and establishing breakpoints and watchpoints. A breakpoint specifies that execution should stop if it reaches a particular location in the source code. A watchpoint specifies that execution should stop if a particular variable is read or written. Both breakpoints and watchpoints can typically be made conditional, so that execution stops only if a particular Boolean predicate evaluates to true.

数据和控制操作都严重依赖于符号信息。符号调试器需要能够解析源语言表达式并将它们与原始程序中的符号关联起来。例如,在gdb中,命令 print ab[i]需要解析要打印的表达式;它还需要识别出ai在程序当前停止点的范围内,并且b是一个数组类型字段,其索引范围包括i的当前值。类似地,命令break 123 if i+j == 3需要解析表达式i+j;它还需要识别出当前源文件的第 123 行有一个可执行语句,并且ij在该行的范围内。

Both data and control operations depend critically on symbolic information. A symbolic debugger needs to be able both to parse source language expressions and to relate them to symbols in the original program. In gdb, for example, the command print a.b[i] needs to parse the to-be-printed expression; it also needs to recognize that a and i are in scope at the point where the program is currently stopped, and that b is an array-typed field whose index range includes the current value of i. Similarly, the command break 123 if i+j == 3 needs to parse the expression i+j; it also needs to recognize that there is an executable statement at line 123 in the current source file, and that i and j are in scope at that line.

数据和控制操作都依赖于从外部操纵程序的能力:停止和启动程序,以及读取和写入数据。这种控制至少可以通过三种方式实现。最简单的方式是在解释器中实现。由于解释器可以直接访问程序的符号表,并且“参与”每个语句的执行,因此在程序和调试器之间来回移动并让后者访问前者的数据是件很简单的事情。

Both data and control operations also depend on the ability to manipulate a program from outside: to stop and start it, and to read and write its data. This control can be implemented in at least three ways. The easiest occurs in interpreters. Since an interpreter has direct access to the program's symbol table and is “in the loop” for the execution of every statement, it is a straightforward matter to move back and forth between the program and the debugger, and to give the latter access to the former's data.

动态二进制重写技术(如 Dynamo 和 Pin)也可用于实现调试器控制 [ ZRA + 08 ]。然而,这项技术相对较新,在生产调试工具中尚未得到广泛应用。

The technology of dynamic binary rewriting (as in Dynamo and Pin) can also be used to implement debugger control [ZRA+08]. This technology is relatively new, however, and is not widely employed in production debugging tools.

对于编译程序,调试器控制的第三种实现是迄今为止最常见的。它依赖于操作系统的支持。在 Unix 中,它使用称为ptrace的内核服务。ptrace内核调用允许调试器“抓取”(附加到)现有进程或启动其控制下的进程。跟踪进程(调试器)可以拦截操作系统发送给被跟踪进程的任何信号,并可以读取和写入其寄存器和内存。如果被跟踪的进程当前正在运行,则调试器可以通过向其发送信号来停止它。如果当前已停止,则调试器可以指定应恢复执行的地址,并可以要求内核以单条指令(称为单步执行的进程运行它或直到它收到另一个信号。

For compiled programs, the third implementation of debugger control is by far the most common. It depends on support from the operating system. In Unix, it employs a kernel service known as ptrace. The ptrace kernel call allows a debugger to “grab” (attach to) an existing process or to start a process under its control. The tracing process (the debugger) can intercept any signals sent to the traced process by the operating system and can read and write its registers and memory. If the traced process is currently running, the debugger can stop it by sending it a signal. If it is currently stopped, the debugger can specify the address at which it should resume execution, and can ask the kernel to run it for a single instruction (a process known as single stepping) or until it receives another signal.

例 16.32

Example 16.32

设置断点

Setting a breakpoint

从用户的角度来看,调试中最神秘的部分可能是用于实现断点、观察点和单步执行的机制。默认实现适用于任何现代处理器,它依赖于修改被跟踪进程的内存空间(特别是包含程序代码的部分)的能力。例如,假设被跟踪的进程当前已停止,并且在恢复之前,调试器希望在函数foo的开头设置断点。它通过将函数序言的第一条指令替换为一种特殊的trap来实现这一点。

Perhaps the most mysterious parts of debugging from the user's perspective are the mechanisms used to implement breakpoints, watchpoints, and single stepping. The default implementation, which works on any modern processor, relies on the ability to modify the memory space of the traced process—in particular, the portion containing the program's code. As an example, suppose the traced process is currently stopped, and that before resuming it the debugger wishes to set a breakpoint at the beginning of function foo. It does so by replacing the first instruction of the function's prologue with a special kind of trap.

设计与实现

Design & Implementation

16.6 矮人

16.6 DWARF

要启用符号调试,编译器必须在每个目标文件中包含符号表信息,格式必须是调试器可以理解的。DWARF 格式是目前最通用的格式之一,许多系统(包括 Linux)都使用它 [ DWA10Eag12 ]。DWARF 最初由贝尔实验室的 Brian Russell 于 20 世纪 80 年代末开发,目前由 Michael Eager 领导的独立 DWARF 委员会维护。版本 5 预计将于 2015 年底推出。

To enable symbolic debugging, the compiler must include symbol table information in each object file, in a format the debugger can understand. The DWARF format, used by many systems (Linux among them) is among the most versatile available [DWA10, Eag12]. Originally developed by Brian Russell of Bell Labs in the late 1980s, DWARF is now maintained by the independent DWARF Committee, led by Michael Eager. Version 5 is expected to appear in late 2015.

与许多专有格式不同,DWARF 旨在适应各种(静态类型)编程语言和同样多种多样的机器架构。除其他外,它还编码所有程序类型的表示、所有程序对象(广义上)的名称、类型和范围、所有堆栈框架的布局以及从源文件和行号到指令地址的映射。

Unlike many proprietary formats, DWARF is designed to accommodate a very wide range of (statically typed) programming languages and an equally wide variety of machine architectures. Among other things, it encodes the representation of all program types, the names, types, and scopes of all program objects (in the broad sense of the term), the layout of all stack frames, and the mapping from source files and line numbers to instruction addresses.

简洁编码备受重视。程序对象以类似于 AST 的方式分层描述。字符串名称和其他重复元素只捕获一次,然后间接引用。整数常量和引用采用可变长度编码,因此较小的值占用较少的位。也许最重要的是,堆栈布局和源到地址映射不是编码为显式表,而是编码为逐行生成表的有限自动机。对于示例 1.20中的微型gcd程序,DWARF 调试信息的人性化表示(由 Linux 的readelf工具生成)将填满本书的整整四页以上。目标文件中的二进制编码仅占用 571 个字节。

Much emphasis has been placed on terse encoding. Program objects are described hierarchically, in a manner reminiscent of an AST. Character string names and other repeated elements are captured exactly once, and then referenced indirectly. Integer constants and references employ a variable-length encoding, so small values take fewer bits. Perhaps most important, stack layouts and source-to-address mappings are encoded not as explicit tables, but as finite automata that generate the tables, line by line. For the tiny gcd program of Example 1.20, a human-readable representation of the DWARF debugging information (as produced by Linux's readelf tool) would fill more than four full pages of this book. The binary encoding in the object file takes only 571 bytes.

陷阱指令是进程向操作系统请求服务的正常方式。在这种特殊情况下,内核将陷阱解释为停止当前正在运行的进程并将控制权返回给调试器的请求。为了在断点之后恢复被跟踪的进程,调试器会放回原始指令,要求内核单步执行被跟踪的进程,再次用陷阱替换该指令以重新启用断点),最后恢复该进程。对于条件断点,调试器会在断点发生时评估条件的谓词。如果断点是无条件的,或者条件为真,调试器跳转到其命令循环并等待用户输入。如果谓词为假,它会自动透明地恢复跟踪的进程。如果断点设置在内循环中,控制将频繁到达该循环,但条件很少为真,则在跟踪的进程和调试器之间来回切换的开销可能非常高。■

Trap instructions are the normal way a process requests a service from the operating system. In this particular case, the kernel interprets the trap as a request to stop the currently running process and return control to the debugger. To resume the traced process in the wake of the breakpoint, the debugger puts back the original instruction, asks the kernel to single-step the traced process, replaces the instruction yet again with a trap (to reenable the breakpoint), and finally resumes the process. For a conditional breakpoint, the debugger evaluates the condition's predicate when the breakpoint occurs. If the breakpoint is unconditional, or if the condition is true, the debugger jumps to its command loop and waits for user input. If the predicate is false, it resumes the traced process automatically and transparently. If the breakpoint is set in an inner loop, where control will reach it frequently, but the condition is seldom true, the overhead of switching back and forth between the traced process and the debugger can be very high. ■

例 16.33

Example 16.33

硬件断点

Hardware breakpoints

某些处理器提供硬件支持,使断点更快一些。例如,x86 有四个调试寄存器,可以设置为(在内核模式下)包含指令地址。如果执行到达该地址,处理器将模拟一个陷阱指令,这样调试器就无需修改被跟踪进程的地址空间,并且消除了恢复原始指令、单步执行进程和将陷阱放回原位所需的额外内核调用(以及被跟踪进程和调试器之间的额外往返)。类似地,许多处理器(包括 x86)都可以置于单步执行模式,该模式在每个用户模式指令之后模拟一个陷阱。如果没有这种支持,调试器(或内核)必须通过在下一条指令处反复放置(临时)断点来实现单步执行。■

Some processors provide hardware support to make breakpoints a bit faster. The x86, for example, has four debugging registers that can be set (in kernel mode) to contain an instruction address. If execution reaches that address, the processor simulates a trap instruction, saving the debugger the need to modify the address space of the traced process and eliminating the extra kernel calls (and the extra round trip between the traced process and the debugger) needed to restore the original instruction, single-step the process, and put the trap back in place. In a similar vein, many processors, including the x86, can be placed in single-stepping mode, which simulates a trap after every user-mode instruction. Without such support, the debugger (or the kernel) must implement single-stepping by repeatedly placing a (temporary) breakpoint at the next instruction. ■

例 16.34

Example 16.34

设置观察点

Setting a watchpoint

观察点有点棘手。到目前为止,最简单的实现取决于硬件支持。假设我们想在程序修改某个变量x时进入调试器。可以设置 x86 和其他现代处理器的调试寄存器,以便在程序写入x的地址时模拟陷阱。当处理器缺乏这样的硬件支持,或者当用户要求调试器设置比硬件所能支持的更多的断点或观察点时,有几种替代方案,但没有一种具有吸引力。也许最明显的是反复单步执行该过程,在每条指令后检查x是否已被修改。如果处理器也没有单步模式,调试器将希望将其临时断点放置在连续的存储指令上,而不是每条指令上(如果它能证明某些存储指令不可能到达x的地址,它也许能够跳过它们)。或者,调试器可以修改跟踪进程的地址空间,使x的页面不可写。然后,进程将在每次写入该页面时发生分段错误,从而允许调试器进行干预。如果写入实际上是写入x,则调试器跳转到其命令循环。否则,它会代表进程执行写入并要求内核恢复它。■

Watchpoints are a bit trickier. By far the easiest implementation depends on hardware support. Suppose we want to drop into the debugger whenever the program modifies some variable x. The debugging registers of the x86 and other modern processors can be set to simulate a trap whenever the program writes to x's address. When the processor lacks such hardware support, or when the user asks the debugger to set more breakpoints or watchpoints than the hardware can support, there are several alternatives, none of them attractive. Perhaps the most obvious is to single step the process repeatedly, checking after each instruction to see whether x has been modified. If the processor also lacks a single-step mode, the debugger will want to place its temporary breakpoints at successive store instructions rather than at every instruction (it maybe able to skip some of the store instructions if it can prove they cannot possibly reach the address of x). Alternatively, the debugger can modify the address space of the traced process to make x's page unwritable. The process will then take a segmentation fault on every write to that page, allowing the debugger to intervene. If the write is actually to x, the debugger jumps to its command loop. Otherwise it performs the write on the process's behalf and asks the kernel to resume it. ■

不幸的是,跟踪进程和调试器之间反复切换上下文的开销会严重影响软件观察点的性能:速度减慢 1000 倍的情况并不罕见。基于动态二进制重写的调试器有可能支持任意数量的观察点,速度接近硬件观察点寄存器允许的速度。这个想法很简单:跟踪程序作为部分执行跟踪在调试器管理的跟踪缓存中运行。在生成每个跟踪时,调试器会在每次存储时以内联方式添加指令,以检查它是否写入x的地址,如果是,则跳回命令循环。

Unfortunately, the overhead of repeated context switches between the traced process and the debugger dramatically impacts the performance of software watchpoints: slowdowns of 1000× are not uncommon. Debuggers based on dynamic binary rewriting have the potential to support arbitrary numbers of watchpoints at speeds close to those admitted by hardware watchpoint registers. The idea is straightforward: the traced program runs as partial execution traces in a trace cache managed by the debugger. As it generates each trace, the debugger adds instructions at every store, in-line, to check whether it writes to x's address and, if so, to jump back to the command loop.

16.3.3 性能分析

16.3.3 Performance Analysis

在将已调试的程序投入生产使用之前,人们通常希望了解(并尽可能提高)其性能。用于分析和剖析程序的工具既多又多种多样,以至于无法在此一一介绍。因此,我们将重点介绍本章中描述的运行时技术,这些技术在许多分析工具中占有突出地位。

Before placing a debugged program into production use, one often wants to understand—and if possible improve—its performance. Tools to profile and analyze programs are both numerous and varied—far too much so to even survey them here. We focus therefore on the run-time technologies, described in this chapter, that feature prominently in many analysis tools.

例 16.35

Example 16.35

统计抽样

Statistical sampling

测量代码各部分所用时间的最简单方法(至少大致如此)可能是定期对程序计数器 (PC)进行采样。Unix中的经典prof工具就是这种方法的典型代表。通过与特殊的prof库链接,程序可以安排接收定期计时器信号(例如,每毫秒一次),作为响应,它将增加与当前 PC 关联的计数器。执行后,prof后处理器会将计数器与程序代码的地址映射相关联,并生成每个子例程和循环所用时间百分比的统计摘要。■

Perhaps the simplest way to measure, at least approximately, the amount of time spent in each part of the code is to sample the program counter (PC) periodically. This approach was exemplified by the classic prof tool in Unix. By linking with a special prof library, a program could arrange to receive a periodic timer signal—once a millisecond, say—in response to which it would increment a counter associated with the current PC. After execution, the prof post-processor would correlate the counters with an address map of the program's code and produce a statistical summary of the percentage of time spent in each subroutine and loop. ■

例 16.36

Example 16.36

调用图分析

Call graph profiling

虽然简单,但prof 也有一些严重的局限性。它的结果只是近似的,无法捕获细粒度的成本。它也无法区分从多个位置对给定例程的调用。如果我们想知道ABC中哪一个对程序运行时间的贡献最大,那么了解它们三个都调用了D(实际上大部分时间都花在了 D 上)并不是特别有帮助。如果我们想知道是ADBD还是CD如此昂贵,我们可以使用(稍微)更新的gprof工具,它依赖于编译器支持来检测过程序言。在被检测的程序运行时,它会记录从每个位置调用D的次数。然后, gprof后处理器假定在D中花费的总时间可以根据相对调用次数准确地在调用站点之间分配。更复杂的工具不仅记录调用者和被调用者,还记录堆栈回溯(动态链的内容),使它们能够应对从A调用D所花费的时间是从BC调用 D 的两倍的情况(参见练习 16.13)。■

While simple, prof had some serious limitations. Its results were only approximate, and could not capture fine-grain costs. It also failed to distinguish among calls to a given routine from multiple locations. If we want to know which of A, B, and C is the biggest contributor to program run time, it is not particularly helpful to learn that all three of them call D, where most of the time is actually spent. If we want to know whether it is A's Ds, B's Ds, or C's Ds that are so expensive, we can use the (slightly) more recent gprof tool, which relies on compiler support to instrument procedure prologues. As the instrumented program runs, it logs the number of times that D is called from each location. The gprof post-processor then assumes that the total time spent in D can accurately be apportioned among the call sites according to the relative number of calls. More sophisticated tools log not only the caller and callee but also the stack backtrace (the contents of the dynamic chain), allowing them to cope with the case in which D consumes twice as much time when called from A as it does when called from B or C (see Exercise 16.13). ■

如果我们的程序由于算法原因而表现不佳,那么了解它大部分时间花在哪里可能就足够了。我们可以把注意力集中在在最重要的地方改进源代码。但是,如果程序由于其他原因而表现不佳,我们通常需要知道原因。可能是由于局部性差而导致缓存未命中?分支预测错误?处理器管道使用不当?解决这些问题和类似问题的工具通常依赖于更广泛的代码检测或某种硬件支持。

If our program is underperforming for algorithmic reasons, it maybe enough to know where it is spending the bulk of its time. We can focus our attention on improving the source code in the places it will matter most. If the program is underperforming for other reasons, however, we generally need to know why. Is it cache misses due to poor locality, perhaps? Branch mispredictions? Poor use of the processor pipeline? Tools to address these and similar questions generally rely on more extensive instrumentation of the code or on some sort of hardware support.

例 16.37

Example 16.37

查找低 IPC 的基本块

Finding basic blocks with low IPC

作为一个检测的例子,考虑识别每个周期执行指令数量异常少的基本块的任务。为了找到这样的块,我们可以结合 (1) 每个块所花费的总时间(通过统计抽样获得),(2) 每个块执行的次数(通过检测获得),以及 (3) 每个块中指令数量的静态知识。如果基本块i包含k i条指令并在程序运行期间执行n i次,它就会为该运行贡献k i n i 条动态指令。设是运行中的指令总数。如果统计抽样表明块i占运行时间的x i % 并且x i明显大于 ( k i n i )/ N,那么就会发生一些奇怪的事情 — 可能是缓存未命中次数异常多。■=nsi1_e

As an example of instrumentation, consider the task of identifying basic blocks that execute an unusually small number of instructions per cycle. To find such blocks we can combine (1) the aggregate time spent in each block (obtained by statistical sampling), (2) a count of the number of times each block executes (obtained via instrumentation), and (3) static knowledge of the number of instructions in each block. If basic block i contains ki instructions and executes ni times during a run of a program, it contributes kini dynamic instructions to that run. Let N=ikini be the total number of instructions in the run. If statistical sampling indicates that block i accounts for xi% of the time in the run and xi is significantly larger than (kini)/N, then something strange is going on—probably an unusually large number of cache misses. ■

例 16.38

Example 16.38

Haswell 性能计数器

Haswell performance counters

大多数现代处理器都提供了一组性能计数器,性能分析工具可以很好地利用这些计数器。例如,Intel Haswell 处理器内置了时钟滴答数(总滴答数和运行时滴答数)和执行指令的计数器。它还有四个通用计数器,内核可以配置它们来计数 250 多种不同类型的事件,包括分支预测错误、TLB(地址转换)未命中以及各种类型的缓存未命中、中断、执行指令和管道停顿。最后,它还具有用于计算处理器核心、缓存和内存所消耗能量焦耳数的计数器。不幸的是,性能计数器通常是一种稀缺资源(人们可能常常希望拥有更多这样的计数器)。它们的数量、类型和操作模式因处理器而异;通常只能在内核模式下直接访问它们;操作系统并不总是通过方便或统一的接口将这种访问导出到用户级程序。大多数制造商(包括 Intel)都提供用于访问计数器和解释其值的工具。便携式工具是一个活跃的研究课题。■

Most modern processors provide a set of performance counters that can be used to good effect by performance analysis tools. The Intel Haswell processor, for example, has built-in counters for clock ticks (both total and when running) and instructions executed. It also has four general-purpose counters, which can be configured by the kernel to count any of more than 250 different kinds of events, including branch mispredictions; TLB (address translation) misses; and various kinds of cache misses, interrupts, executed instructions, and pipeline stalls. Finally, it has counters for the number of Joules of energy consumed by the processor cores, caches, and memory. Unfortunately, performance counters are generally a scarce resource (one might often wish for many more of them). Their number, type, and mode of operation varies greatly from processor to processor; direct access to them is usually available only in kernel mode; and operating systems do not always export that access to user-level programs with a convenient or uniform interface. Tools to access the counters and interpret their values are available from most manufacturers—Intel among them. Portable tools are an active topic of research. ■

16-01-9780124104099检查你的理解

Check Your Understanding

27. 什么是反射?它有什么用途?

27. What is reflection? What purposes does it serve?

28. 描述反射的一个不恰当的使用。

28. Describe an inappropriate use of reflection.

29. 说出 CLI 支持但 JVM 不支持的反射的一个方面。

29. Name an aspect of reflection supported by the CLI but not by the JVM.

30. 为什么在 Java 或 C# 中实现反射比在 Perl 或 Ruby 中更困难?

30. Why is reflection more difficult to implement in Java or C# than it is in Perl or Ruby?

31. 什么是注释(Java)或属性(C#)?它们有什么用处?

31. What are annotations (Java) or attributes (C#)? What are they used for?

32. 什么是javadoc、apt、JMLLINQ,它们和注解有什么关系?

32. What are javadoc, apt, JML, and LINQ, and what do they have to do with annotation?

33. 简要描述符号调试器的三种不同的实现策略。

33. Briefly describe three different implementation strategies for a symbolic debugger.

34.解释一下 断点观察点之间的区别。为什么观察点可能更昂贵?

34. Explain the difference between breakpoints and watchpoints. Why are watchpoints potentially much more expensive?

35.总结Unix  ptrace机制提供的功能。

35. Summarize the capabilities provided by the Unix ptrace mechanism.

36.  Unix profgprof工具之间的主要区别是什么?

36. What is the principal difference between the Unix prof and gprof tools?

37. 出于性能分析的目的,总结统计抽样、仪表和硬件性能计数器的相对优势和局限性。解释为什么统计抽样和仪表可以结合起来使用。

37. For the purposes of performance analysis, summarize the relative strengths and limitations of statistical sampling, instrumentation, and hardware performance counters. Explain why statistical sampling and instrumentation might profitably be used together.

16.4 总结和结束语

16.4 Summary and Concluding Remarks

我们在本章开头将运行时系统定义为一组库,这些库对于许多语言实现至关重要,它们依赖于编译器或编译器生成的程序的知识。我们将这些库与“普通”库区分开来,普通库只需要传递的参数。

We began this chapter by defining a run-time system as the set of libraries, essential to many language implementations, that depend on knowledge of the compiler or the programs it produces. We distinguished these from “ordinary” libraries, which require only the arguments they are passed.

我们注意到,本书其他部分涵盖的几个主题,包括垃圾回收、可变长度参数列表、异常和事件处理、协同程序和线程、远程过程调用、事务内存和动态链接,通常被认为是运行时系统的权限。然后我们转向虚拟机,特别关注 Java 虚拟机 (JVM) 和公共语言基础结构 (CLI)。在机器代码后期绑定的一般标题下,我们考虑了即时和动态编译、二进制翻译和重写以及移动代码和沙盒。最后,在检查和自省的一般标题下,我们考虑了反射机制、符号调试和性能分析。

We noted that several topics covered elsewhere in the book, including garbage collection, variable-length argument lists, exception and event handling, coroutines and threads, remote procedure calls, transactional memory, and dynamic linking are often considered the purview of the run-time system. We then turned to virtual machines, focusing in particular on the Java Virtual Machine (JVM) and the Common Language Infrastructure (CLI). Under the general heading of late binding of machine code, we considered just-in-time and dynamic compilation, binary translation and rewriting, and mobile code and sandboxing. Finally, under the general heading of inspection and introspection, we considered reflection mechanisms, symbolic debugging, and performance analysis.

通过所有这些主题,我们看到了随着时间的推移复杂性的稳步增加。早期的 Basic 解释器一次解析和执行一个源语句。现代解释器首先将其源代码转换为语法树。早期的 Java 实现虽然仍然基于解释器,但依赖于单独的源代码到字节码编译器。现代 Java 实现以及 CLI 的实现都通过即时编译器提高了性能。对于在运行时扩展自身的程序,CLI 也允许动态调用源代码到字节码编译器,就像在 Common Lisp 中一样。最近的系统可能会对已经运行的程序进行分析和重新优化。类似的技术可能允许单独的工具从一种机器语言转换为另一种机器语言,或者对代码进行测试、调试、安全性、性能分析、模型检查或架构模拟。CLI 为跨语言互操作性提供了广泛的支持。

Through all these topics we have seen a steady increase in complexity over time. Early Basic interpreters parsed and executed one source statement at a time. Modern interpreters first translate their source into a syntax tree. Early Java implementations, while still interpreter-based, relied on a separate source-to-byte-code compiler. Modern Java implementations, as well as implementations of the CLI, enhance performance with a just-in-time compiler. For programs that extend themselves at run time, the CLI allows the source-to-byte-code compiler to be invoked dynamically as well, as it is in Common Lisp. Recent systems may profile and reoptimize already-running programs. Similar technology may allow separate tools to translate from one machine language to another, or to instrument code for testing, debugging, security, performance analysis, model checking, or architectural simulation. The CLI provides extensive support for cross-language interoperability.

许多这些发展已经模糊了编译器和运行时系统之间的界限,以及编译时和运行时操作之间的界限。可以肯定地说,这些趋势将继续下去。越来越多的人不再将程序视为静态产物,而是将其视为可塑组件的动态集合,具有丰富的语义结构,可以进行形式分析和重新配置。

Many of these developments have served to blur the line between the compiler and the run-time system, and between compile-time and run-time operations. It seems safe to predict that these trends will continue. More and more, programs will come to be seen not as static artifacts, but as dynamic collections of malleable components, with rich semantic structure amenable to formal analysis and reconfiguration.

16.5 练习

16.5 Exercises

16.1将 示例 15.4中的公式写成表达式树(一种语法树,其中每个运算符都由一个内部节点表示,该节点的子节点是其操作数)。通过合并相同的节点,将树转换为表达式DAG。评论树中的冗余以及它与图 15.4的关系。

16.1 Write the formula of Example 15.4 as an expression tree (a syntax tree in which each operator is represented by an internal node whose children are its operands). Convert your tree to an expression DAG by merging identical nodes. Comment on the redundancy in the tree and how it relates to Figure 15.4.

16.2在 示例 15.4图 15.4中,我们假设a b cs都是当前方法的前几个局部变量,可以用一条单字节指令将它们压入操作数栈或从操作数栈弹出。假设情况并非如此:即推送和弹出指令各需要三个字节。图 15.4左侧的代码现在需要多少字节?

大多数基于堆栈的语言(其中包括 Java 字节码和 CIL)都提供了交换指令(用于反转堆栈顶部两个值的顺序)和复制指令(用于推送当前位于堆栈顶部的值的第二个副本)。说明如何使用交换复制来消除图 15.4左侧的弹出推送s的操作。请随意利用乘法的结合性。您的新序列有多少条指令?多少字节?

16.2 We assumed in Example 15.4 and Figure 15.4 that a, b, c, and s were all among the first few local variables of the current method, and could be pushed onto or popped from the operand stack with a single one-byte instruction. Suppose that this is not the case: that is, that the push and pop instructions require three bytes each. How many bytes will now be required for the code on the left side of Figure 15.4?

Most stack-based languages, Java bytecode and CIL among them, provide a swap instruction that reverses the order of the top two values on the stack, and a duplicate instruction that pushes a second copy of the value currently at top of stack. Show how to use swap and duplicate to eliminate the pop and the pushes of s in the left side of Figure 15.4. Feel free to exploit the associativity of multiplication. How many instructions is your new sequence? How many bytes?

16.3 示例 16.5中的推测优化原则上可以静态执行。解释为什么动态编译器可以更有效地完成此操作。

16.3 The speculative optimization of Example 16.5 could in principle be statically performed. Explain why a dynamic compiler might be able to do it more effectively.

16.4 运行时插桩最常见的形式可能是计算每个基本块的执行次数。由于基本块很短,因此向每个块添加加载-增量-存储指令序列会对运行时间产生重大影响。

我们可以通过注意某些块意味着执行其他块来提高性能。例如,在if…then…else结构中,执行then部分或else部分意味着执行条件测试。如果我们聪明的话,我们实际上不必插桩测试。

描述一种通用技术,以尽量减少必须插桩的块数,以允许后处理器获得准确的计算每个块的计数。(这是一个难题。有关提示,请参阅 Larusand Ball 的论文 [ BL92 ]。)

16.4 Perhaps the most common form of run-time instrumentation counts the the number of times that each basic block is executed. Since basic blocks are short, adding a load-increment-store instruction sequence to each block can have a significant impact on run time.

We can improve performance by noting that certain blocks imply the execution of other blocks. In an if… then … else construct, for example, execution of either the then part or the else part implies execution of the conditional test. If we're smart, we won't actually have to instrument the test.

Describe a general technique to minimize the number of blocks that must be instrumented to allow a post-processor to obtain an accurate count for each block. (This is a difficult problem. For hints, see the paper byLarusand Ball [BL92].)

16.5 访问 software.intel.com/en-us/articles/pintool-downloads 并下载 Pin 的副本。使用它创建一个工具来分析循环。当给定一个(机器代码)程序及其输入时,该工具的输出应列出运行该程序时遇到的每个循环的次数。它还应为每个循环提供执行的迭代次数的直方图。

16.5 Visit software.intel.com/en-us/articles/pintool-downloads and download a copy of Pin. Use it to create a tool to profile loops. When given a (machine code) program and its input, the output of the tool should list the number of times that each loop was encountered when running the program. It should also give a histogram, for each loop, of the number of iterations executed.

16.6 概述二进制重写器可能使用的机制,无需访问源代码,即可捕获未初始化变量的使用、“双重删除”和已释放内存的使用(例如悬空指针)。在什么情况下,您可能能够捕获内存泄漏和越界数组访问?

16.6 Outline mechanisms that might be used by a binary rewriter, without access to source code, to catch uses of uninitialized variables, “double deletes,” and uses of deallocated memory (e.g., dangling pointers). Under what circumstances might you be able to catch memory leaks and out-of-bounds array accesses?

16.7扩展 图 16.4的代码以打印有关

16.7 Extend the code of Figure 16.4 to print information about

(一) 田地

(a) fields

(b) 构造函数

(b) constructors

(三) 嵌套类

(c) nested classes

(d) 实现的接口

(d) implemented interfaces

(e) 祖先类及其方法、字段和构造函数

(e) ancestor classes, and their methods, fields, and constructors

(f) 方法抛出的异常

(f) exceptions thrown by methods

(七) 泛型类型参数

(g) generic type parameters

16.8 用 C# 重复上一个练习。添加有关参数名称和通用实例的信息。

16.8 Repeat the previous exercise in C#. Add information about parameter names and generic instances.

16.9 编写一个交互式工具,接受键盘命令来加载指定的类文件、创建类的实例、调用类的方法以及读写类的字段。可以将键盘输入限制为内置类型的值,并且只能在全局范围内工作。根据您的经验,评论为 Java 编写命令行解释器的可行性,类似于 Lisp、Prolog 或各种脚本语言常用的解释器。

16.9 Write an interactive tool that accepts keyboard commands to load specified class files, create instances of their classes, invoke their methods, and read and write their fields. Feel free to limit keyboard input to values of built-in types, and to work only in the global scope. Based on your experience, comment on the feasibility of writing a command-line interpreter for Java, similar to those commonly used for Lisp, Prolog, or the various scripting languages.

16.10在 Java 中,如果 p的具体类型是Foo则 p.getClass()Foo.class将返回相同的内容。请解释为什么在 Ruby、Python 或 JavaScript 中不能保证类似的等价性。有关提示,请参阅第 14.4.4 节

16.10 In Java, if the concrete type of p is Foo, p.getClass() and Foo.class will return the same thing. Explain why a similar equivalence could not be guaranteed to hold in Ruby, Python, or JavaScript. For hints, see Section 14.4.4.

16.11 设计基于 Java 注释的“测试工具”系统。用户应该能够将注释附加到方法上,该注释指定要传递给方法测试运行的参数以及预期返回的值。为简单起见,您可以假设参数和返回值都是字符串或内置类型的实例。使用 Java 6 的注释处理功能,您应该在任何具有带有@Test注释的方法的类中自动生成一个新方法test()此方法应使用指定的参数调用注释的方法,测试返回值并报告任何差异。它还应调用任何嵌套类的测试方法。确保包含一种机制来调用每个顶级类的测试方法。对于额外的挑战,除了返回的值之外,还要设计一种方法来指定单个方法的多个测试,以及一种测试抛出的异常的方法。

16.11 Design a “test harness” system based on Java annotations. The user should be able to attach to a method an annotation that specifies parameters to be passed to a test run of the method, and values expected to be returned. For simplicity, you may assume that parameters and return values will all be strings or instances of built-in types. Using the annotation processing facility of Java 6, you should automatically generate a new method, test() in any class that has methods with @Test annotations. This method should call the annotated methods with the specified parameters, test the return values, and report any discrepancies. It should also call the test methods of any nested classes. Be sure to include a mechanism to invoke the test method of every top-level class. For an extra challenge, devise a way to specify multiple tests of a single method, and a way to test exceptions thrown, in addition to values returned.

16.12  C++ 提供了一个typeid运算符,可用于查询指针或引用变量的具体类型:if (typeid(*p) == typeid(my_derived_type)) … typeid返回的值可以比较相等性,但不能赋值。它们还支持name()方法,该方法返回类型的(依赖于实现的)字符串名称。给出一个可以合理使用这些机制的程序片段示例。与更广泛的反射机制不同,typeid只能应用于具有至少一个虚方法的类(实例)。给出对这一限制的合理解释。

 



16.12 C++ provides a typeid operator that can be used to query the concrete type of a pointer or reference variable:

 if (typeid(*p) == typeid(my_derived_type)) …

Values returned by typeid can be compared for equality but not assigned. They also support a name() method that returns an (implementation-dependent) character string name for the type. Give an example of a program fragment in which these mechanisms might reasonably be used.

Unlike more extensive reflection mechanisms, typeid can be applied only to (instances of) classes with at least one virtual method. Give a plausible explanation for this restriction.

16.13假设我们希望如 示例 16.36末尾所述,准确地将采样时间归因于调用子程序的各种上下文。也许最直接的方法是不仅记录当前 PC,还记录每次定时器中断时的堆栈回溯(动态链的内容)。不幸的是,这会大大增加分析开销。建议采用等效但更便宜的实现方式。

16.13 Suppose we wish, as described at the end of Example 16.36, to accurately attribute sampled time to the various contexts in which a subroutine is called. Perhaps the most straightforward approach would be to log not only the current PC but also the stack backtrace—the contents of the dynamic chain—on every timer interrupt. Unfortunately, this can dramatically increase profiling overhead. Suggest an equivalent but cheaper implementation.

16-02-9780124104099 16.14–16.17 更深入。

16.14–16.17  In More Depth.

16.6 探索

16.6 Explorations

16.18 了解 Java安全策略机制。程序员可以启用/禁止哪些方面的程序行为?这些策略是如何执行的?安全策略与边栏 16.3 中描述的验证过程之间的关系(如果有的话)是什么?

16.18 Learn about the Java security policy mechanism. What aspects of program behavior can the programmer enable/proscribe? How are such policies enforced? What is the relationship (if any) between security policies and the verification process described in Sidebar 16.3?

16.19 了解Perl 和 Ruby 中的污点模式。它与侧栏 16.5 中描述的通过二进制重写创建沙箱相比如何?它能捕获哪些类型的安全问题?它不能捕获哪些类型的问题?

16.19 Learn about taint mode in Perl and Ruby. How does it compare to sandbox creation via binary rewriting, as described in Sidebar 16.5? What sorts of security problems does it catch? What sorts of problems does it not catch?

16.20 了解带有证明的代码,这是一种技术,其中移动代码的供应商包含其安全性的证明,而用户只需验证该证明,而不是重新生成它(从 Necula [ Nec97 ] 的工作开始)。与其他形式的沙盒相比,这种技术如何?它可以保证哪些属性?

16.20 Learn about proof-carrying code, a technique in which the supplier of mobile code includes a proof of its safety, and the user simply verifies the proof, rather than regenerating it (start with the work of Necula [Nec97]). How does this technique compare to other forms of sandboxing? What properties can it guarantee?

16.21 研究Common Lisp 对象系统的基础MetaObject 协议(MOP)。它与 Java 和 C# 的反射机制相比如何?它能让你做哪些其他语言做不到的事情?

16.21 Investigate the MetaObject Protocol (MOP), which underlies the Common Lisp Object System. How does it compare to the reflection mechanisms of Java and C#? What does it allow you to do that these other languages do not?

16.22 使用符号调试器并设置断点来运行程序时,经常会发现“走得太远”,必须从头开始运行程序。这可能意味着为达到某一特定点而付出的所有努力都将付诸东流。考虑一下如何才能使程序不仅向前运行,而且向后运行。这种反向执行功能可能使用户能够缩小错误源的范围,就像缩小二分搜索的范围一样。考虑数据被覆盖时发生的信息丢失以及并行和事件驱动程序中出现的不确定性。

16.22 When using a symbolic debugger and moving through a program with breakpoints, one often discovers that one has gone “too far,” and must start the program over from the beginning. This may mean losing all the effort that had been put into reaching a particular point. Consider what it would take to be able to run the program not only forward but backward as well. Such a reverse execution facility might allow the user to narrow in on the source of bugs much as one narrows the range in binary search. Consider both the loss of information that happens when data is overridden and the nondeterminism that arises in parallel and event-driven programs.

16.23 下载并试用 Linux 中用于性能计数器采样的几个可用软件包之一(尝试 sourceforge.net/projects/perfctr/、perfmon2.sourceforge.net/ 或www.intel.com/software/pcm)。这些软件包允许您测量什么?您将如何使用这些信息?(注意:您可能需要安装内核补丁以使程序计数器可用于用户级代码。)

16.23 Download and experiment with one of the several available packages for performance counter sampling in Linux (try sourceforge.net/projects/perfctr/, perfmon2.sourceforge.net/, or www.intel.com/software/pcm). What do these packages allow you to measure? How might you use the information? (Note: you may need to install a kernel patch to make the program counters available to user-level code.)

16-02-9780124104099 16.24–16.25 更深入。

16.24–16.25  In More Depth.

16.7 书目注释

16.7 Bibliographic Notes

Aycock [ Ayc03 ] 概述了即时编译的历史。有关 HotSpot 编译器和 JVM 的文档可在 Oracle 的网站上找到:www.oracle.com/technetwork/articles/javase/index-jsp-136373.html。JVM规范由 Lindholm 等人制定。[ LYBB14 ]。有关 CLI 的信息来源包括 ECMA 标准 [ Int12aMR04 ] 和 msdn.microsoft.com 上的 .NET 页面。

Aycock [Ayc03] surveys the history of just-in-time compilation. Documentation on the HotSpot compiler and JVM can be found at Oracle's web site: www.oracle.com/technetwork/articles/javase/index-jsp-136373.html. The JVM specification is by Lindholm et al. [LYBB14]. Sources of information on the CLI include the ECMA standard [Int12a, MR04] and the .NET pages at msdn.microsoft.com.

Arnold 等人 [ AFG + 05 ] 对虚拟机上程序的自适应优化技术进行了广泛的调查。Deutsch 和 Schiffman [ DS84 ] 描述了 ParcPlace Smalltalk 虚拟机,该虚拟机开创了即时编译和缓存 JIT 编译的机器代码等机制。各种文章讨论了 Apple 的 68K 仿真器 [ Tho95 ]、DEC 的 FX!32 [ HH97b ] 及其早期的 VEST 和mx [ SCK + 93 ] 的二进制翻译技术。

Arnold et al. [AFG+05] provide an extensive survey of adaptive optimization techniques for programs on virtual machines. Deutsch and Schiffman [DS84] describe the ParcPlace Smalltalk virtual machine, which pioneered such mechanisms as just-in-time compilation and the caching of JIT-compiled machine code. Various articles discuss the binary translation technology of Apple's 68K emulator [Tho95], DEC's FX!32 [HH97b], and its earlier VEST and mx [SCK+93].

关于二进制重写的最佳信息来源可能是 Hazelwood 的文章 [ Haz11 ]。关于 Dynamo、Atom、Pin 和 QEMU 的原始论文分别由 Bala 等人 [ BDB00 ]、Srivastava 和 Eustace [ SE94 ]、Luk 等人 [ LCM + 05 ] 和 Bellard [ Bel05 ] 撰写。Duesterwald [ Due05 ] 借鉴了她在 Dynamo 项目中的经验,调查了动态二进制优化器设计中的问题。Wahbe 等人 [ WLAG93 ]报告了通过二进制重写进行沙盒处理的早期工作。

Probably the best source of information on binary rewriting is the text by Hazelwood [Haz11]. The original papers on Dynamo, Atom, Pin, and QEMU are by Bala et al. [BDB00], Srivastava and Eustace [SE94], Luk et al. [LCM+05], and Bellard [Bel05], respectively. Duesterwald [Due05] surveys issues in the design of a dynamic binary optimizer, drawing on her experience with the Dynamo project. Early work on sandboxing via binary rewriting is reported by Wahbe et al. [WLAG93].

DWARF 标准可从 dwarfstd.org [ DWA10 ] 获取;Eager 提供了更简单的介绍 [ Eag12 ]。Ball 和 Larus [ BL92 ] 描述了分析基本块执行所需的最少检测。Zhao 等人 [ ZRA + 08 ] 描述了如何使用动态检测(基于 DynamoRIO)来有效实现观察点。Martonosi 等人 [ MGA92 ] 描述了一种基于示例 16.37中概述的思想的性能分析工具。

The DWARF standard is available from dwarfstd.org [DWA10]; Eager provides a gentler introduction [Eag12]. Ball and Larus [BL92] describe the minimal instrumentation required to profile the execution of basic blocks. Zhao et al. [ZRA+08] describe the use of dynamic instrumentation (based on DynamoRIO) to implement watchpoints efficiently. Martonosi et al. [MGA92] describe a performance analysis tool that builds on the idea outlined in Example 16.37.


1具体来说,CLI 定义了编译器目标语言的指令集:第C-16.1.2 节中描述的通用中间语言 (CIL) 。

1 In particular, the CLI defines the instruction set of compiler's target language: the Common Intermediate Language (CIL) described in Section C-16.1.2.

2 CLI 是 ECMA 和 ISO 标准。CLR(通用语言运行时)是 Microsoft 对 CLI 的实现。它是 .NET 框架的一部分。

2 CLI is an ECMA and ISO standard. CLR—the Common Language Runtime—is Microsoft's implementation of the CLI. It is part of the .NET framework.

3类型不安全的代码(如 C 语言)的编译是有问题的;我们将在C-16.1.2 节中再次讨论这个问题。

3 Compilation of type-unsafe code, as in C, is problematic; we will return to this issue in Section C-16.1.2.

4为了节省空间,JIT 编译器通常会省略任何它可以证明从未用于同步的对象上的监视器信息。

4 To save space, a JIT compiler will typically omit the monitor information for any object it can prove is never used for synchronization.

5虽然 JIT 编译器原则上可以对源代码进行操作,但我们在整个讨论过程中都假设它适用于中级 IF,如 Java 字节码或 CIL。

5 While a JIT compiler could, in principle, operate on source code, we assume throughout this discussion that it works on a medium-level IF like Java bytecode or CIL.

17

代码改进

Code Improvement

第 15 章 中,我们讨论了汇编和在编译器后端链接目标代码。我们介绍的技术导致代码正确但非常不理想:存在许多冗余计算,并且对现代微处理器的寄存器、多个功能单元和缓存的使用效率低下。本章将介绍代码改进:编译阶段致力于生成良好(快速)代码。如第 1.6.4 节所述,代码改进通常称为优化,尽管它很少使任何事情在绝对意义上达到最优。

In Chapter 15 we discussed the generation, assembly, and linking of target code in the back end of a compiler. The techniques we presented led to correct but highly suboptimal code: there were many redundant computations, and inefficient use of the registers, multiple functional units, and cache of a modern microprocessor. This chapter takes a look at code improvement: the phases of compilation devoted to generating good (fast) code. As noted in Section 1.6.4, code improvement is often referred to as optimization, though it seldom makes anything optimal in any absolute sense.

我们的研究将考虑简单的窥孔优化,它在非常小的指令窗口内“清理”生成的目标代码;局部优化,为各个基本块生成近乎最优的代码;以及全局优化,在整个子程序级别执行更积极的代码改进。我们不会介绍过程间改进;感兴趣的读者可以参考其他文本(参见本章末尾的参考书目注释)。此外,即使对于我们涵盖的主题,我们的意图也将更多地是“揭开”代码改进的神秘面纱,而不是详细描述该过程。大部分讨论将围绕单个子程序代码的连续改进展开。这个扩展示例将使我们能够说明几种关键形式的代码改进的效果,而无需详细讨论如何实现它们。

Our study will consider simple peephole optimization, which “cleans up” generated target code within a very small instruction window; local optimization, which generates near-optimal code for individual basic blocks; and global optimization, which performs more aggressive code improvement at the level of entire subroutines. We will not cover interprocedural improvement; interested readers are referred to other texts (see the Bibliographic Notes at the end of the chapter). Moreover, even for the subjects we cover, our intent will be more to “demystify” code improvement than to describe the process in detail. Much of the discussion will revolve around the successive refinement of code for a single subroutine. This extended example will allow us to illustrate the effect of several key forms of code improvement without dwelling on the details of how they are achieved.

17-01-9780124104099 更深入地

IN MORE DEPTH

第 17 章的完整内容可在配套网站上找到。

Chapter 17 can be found in its entirety on the companion site.

一个

提到的编程语言

Programming Languages Mentioned

本附录提供了本书中提到的每种主要编程语言的简要说明、参考书目和(在许多情况下)在线信息的 URL。截至 2015 年 6 月,这些 URL 都是准确的,但随着人们移动文件,它们可能会发生变化。在参考书目中可以找到一些其他 URL。

This appendix provides brief descriptions, bibliographic references, and (in many cases) URLs for on-line information concerning each of the principal programming languages mentioned in this book. The URLs are accurate as of June 2015, though they are subject to change as people move files around. Some additional URLs can be found in the bibliographic references.

Bill Kinnersley 在people.ku.edu/~nkinners/LangList/Extras/langlist.htm上维护了大约 2500 种编程语言的在线资料索引。

Bill Kinnersley maintains an index of on-line materials for approximately 2500 programming languages at people.ku.edu/~nkinners/LangList/Extras/langlist.htm.

图 A.1显示了一些较有影响力或广泛使用的编程语言的谱系。每种语言的日期表示其特性广为人知的大致时间。箭头表示对设计的主要影响。当然,许多影响无法在一张图中显示出来。

Figure A.1 shows the genealogy of some of the more influential or widely used programming languages. The date for each language indicates the approximate time at which its features became widely known. Arrows indicate principal influences on design. Many influences, of course, cannot be shown in a single figure.

f24-01-9780124104099
图 A.1 选定的编程语言的谱系日期为大概日期。页面底部延伸的箭头表示正在进行的进化。

Ada 最初旨在成为美国国防部 [ Ame83 ] 委托开发的所有软件的标准语言,现在由 ISO [ Int12b ] 标准化。原型由多个站点的团队设计;最终的 '83 语言由位于明尼阿波利斯的 Honeywell 系统和研究中心和法国的 Alsys 公司的一个团队在 Jean Ichbiah 的领导下开发。一种非常庞大的语言,主要源自 Pascal。设计原理在一份非常清晰的配套文档 [ IBFW91 ] 中阐明。Ada 95 是 Intermetrics, Inc. 的一个团队根据政府合同开发的修订版。它修复了早期语言中的几个细微问题,并添加了对象、共享内存同步和许多其他功能。Ada 2005 和 Ada 2012 添加了许多附加功能;有关摘要,请参阅ada2012.org/comparison.html。免费提供的实现(gnat)作为 GNU 编译器集合(gcc)的一部分分发。更多资源请访问adaic.org/ada-europe.org/

Ada Originally intended to be the standard language for all software commissioned by the U.S. Department of Defense [Ame83], now standardized by the ISO [Int12b]. Prototypes designed by teams at several sites; final '83 language developed by a team at Honeywell's Systems and Research Center in Minneapolis and Alsys Corp. in France, led by Jean Ichbiah. A very large language, descended largely from Pascal. Design rationale articulated in a remarkably clear companion document [IBFW91]. Ada 95 was a revision developed under government contract by a team at Intermetrics, Inc. It fixed several subtle problems in the earlier language, and added objects, shared-memory synchronization, and many other features. Ada 2005 and Ada 2012 add a host of additional features; for a summary see ada2012.org/comparison.html. Freely available implementation (gnat) distributed as part of the GNU compiler collection (gcc). Additional resources at adaic.org/ and ada-europe.org/.

Algol 60 最初的块结构语言。Naur 等人的定义 [ NBB + 63 ] 被认为是清晰简洁的里程碑。它包括巴科斯范式 (BNF) 的原始使用。

Algol 60 The original block-structured language. The definition by Naur et al. [NBB+63] is considered a landmark of clarity and conciseness. It includes the original use of Backus-Naur Form (BNF).

Algol 68  Algol 60 的大型且相对复杂的后继者,由 A. van Wijngaarden 领导的委员会设计。包括(除其他外)结构和联合、基于表达式的语法、引用参数、变量的引用模型和并发性。官方定义 [ vMP + 75 ] 使用使用非常规术语,阅读起来非常困难;其他来源(例如,Pagan 的书 [ Pag76 ])更容易理解。

Algol 68 A large and relatively complex successor to Algol 60, designed by a committee led by A. van Wijngaarden. Includes (among other things) structures and unions, expression-based syntax, reference parameters, a reference model of variables, and concurrency. The official definition [vMP+75] uses unconventional terminology and is very difficult to read; other sources (e.g., Pagan's book [Pag76]) are more accessible.

Algol W  Niklaus Wirth 和 CAR Hoare [ WH66 Sit72 ]提出的 Algol 68 的更小、更简单的替代方案。Pascal 的前身。引入了case语句。

Algol W A smaller, simpler alternative to Algol 68, proposed by Niklaus Wirth and C. A. R. Hoare [WH66, Sit72]. The precursor to Pascal. Introduced the case statement.

APL 由 Kenneth Iverson 在 20 世纪 50 年代末和 60 年代初设计,主要用于操作数值数组。功能强大。极其简洁。运算符集强大。使用扩展字符集。旨在用于交互。原始语法 [ Ive62 ] 是非线性的;由于 IBM 团队的改进,实现通常使用修订的语法 [ IBM87 ]。在线资源位于sigapl.org/

APL Designed by Kenneth Iverson in the late 1950s and early 1960s, primarily for the manipulation of numeric arrays. Functional. Extremely concise. Powerful set of operators. Employs an extended character set. Intended for interactive use. Original syntax [Ive62] was nonlinear; implementations generally use a revised syntax due to a team at IBM [IBM87]. On-line resources at sigapl.org/.

Basic 简单的命令式语言,最初用于交互用途。原始版本由达特茅斯学院的 John Kemeny 和 Thomas Kurtz 于 20 世纪 60 年代初开发。存在数十种方言。Microsoft 的 Visual Basic 与原始版本几乎没有相似之处,是当今使用最广泛的语言(资源可在msdn.microsoft.com/en-us/library/sh9ywfdk.aspx获得)。ANSI 标准 [ Ame78 ]定义的最小子集。

Basic Simple imperative language, originally intended for interactive use. Original version developed by John Kemeny and Thomas Kurtz of Dartmouth College in the early 1960s. Dozens of dialects exist. Microsoft's Visual Basic, which bears little resemblance to the original, is the most widely used today (resources available at msdn.microsoft.com/en-us/library/sh9ywfdk.aspx). Minimal subset defined by ANSI standard [Ame78].

C 最成功的命令式语言之一。最初由贝尔实验室的 Brian Kernighan 和 Dennis Ritchie 在开发 Unix [ KR88 ] 过程中定义。语法简洁。声明语法不常见。旨在用于系统编程。弱类型检查。无动态语义检查。1990 年由 ANSI/ISO 标准化 [ Ame90 ]。1994 年采用国际字符集扩展。1999 年和 2011 年采用了更广泛的更改 [ Int99 Int11 ]。流行的开源实现包括gcc ( gnu.org/software/gcc/ ) 和clang/llvm ( clang.llvm.org/ )。

C One of the most successful imperative languages. Originally defined by Brian Kernighan and Dennis Ritchie of Bell Labs as part of the development of Unix [KR88]. Concise syntax. Unusual declaration syntax. Intended for systems programming. Weak type checking. No dynamic semantic checks. Standardized by ANSI/ISO in 1990 [Ame90]. Extensions for international character sets adopted in 1994. More extensive changes adopted in 1999 and 2011 [Int99, Int11]. Popular open-source implementations include gcc (gnu.org/software/gcc/) and clang/llvm (clang.llvm.org/).

C# 面向对象语言,最初由 Anders Hejlsberg、Scott Wiltamuth 和 Microsoft Corporation 的同事在 20 世纪 90 年代末和 21 世纪初设计 [ HTWG11 Mic12 ECM06a ]。版本 1 和 2 由 ECMA 和 ISO [ ECM06a ] 标准化;后续版本由 Microsoft 直接定义。用作 .NET 平台的主要语言,该平台是用于多语言分布式计算的运行时和中间件系统。包括 Java 的大部分功能以及 C++ 和 Visual Basic 的许多功能,包括引用和值类型、连续和行指针数组、虚拟和非虚拟方法、运算符重载、委托、泛型、局部类型推断、带有指针的“不安全”超集以及跨语言边界调用动态类型对象的方法的能力。商业资源位于msdn.microsoft.com/en-us/vstudio/hh388566.aspx。可在mono-project.com/docs/about-mono/languages/csharp/免费获取实现

C# Object-oriented language originally designed by Anders Hejlsberg, Scott Wiltamuth, and associates at Microsoft Corporation in the late 1990s and early 2000s [HTWG11, Mic12, ECM06a]. Versions 1 and 2 standardized by ECMA and the ISO [ECM06a]; subsequent versions defined directly by Microsoft. Serves as the principal language for the .NET platform, a runtime and middleware system for multilanguage distributed computing. Includes most of Java's features, plus many from C++ and Visual Basic, including both reference and value types, both contiguous and row-pointer arrays, both virtual and nonvirtual methods, operator overloading, delegates, generics, local type inference, an “unsafe” superset with pointers, and the ability to invoke methods of dynamically typed objects across language boundaries. Commercial resources at msdn.microsoft.com/en-us/vstudio/hh388566.aspx. Freely available implementation at mono-project.com/docs/about-mono/languages/csharp/.

C++ 第一个得到广泛采用的面向对象 C 语言后继者。至今仍被广泛认为是最适合“工业强度”计算的语言。最初由贝尔实验室(现位于德克萨斯农工大学)的 Bjarne Stroustrup 设计。一种大型语言。1998 年由 ISO 标准化;2011 年进行了重大修订;2014 年进行了更新。包括(除其他外)通用引用类型、静态和动态方法绑定、广泛的功能用于重载和强制、多重继承和图灵完备泛型。没有自动垃圾收集。有许多文本;除了 ISO 标准 [ Int14b ] 之外,Stroustrup 的文本是最全面的 [ Str13 ]。gcc和clang/llvm发行版中包含免费提供的实现(参见 C)。Stroustrup 自己的资源页面位于stroustrup.com/C++.html。

C++ The first object-oriented successor to C to gain widespread adoption. Still widely considered the one most suited to “industrial strength” computing. Originally designed by Bjarne Stroustrup of Bell Labs (now at Texas A&M University). A large language. Standardized by the ISO in 1998; major revision in 2011; updated in 2014. Includes (among other things) generalized reference types, both static and dynamic method binding, extensive facilities for overloading and coercion, multiple inheritance, and Turing-complete generics. No automatic garbage collection. Many texts exist; aside from the ISO standard [Int14b], Stroustrup's is the most comprehensive [Str13]. Freely available implementations included in the gcc and clang/llvm distributions (see C). Stroustrup's own resource page is at stroustrup.com/C++.html.

Caml 和 OCaml  Caml 是 ML 的一种方言,由 Guy Cousineau 及其同事在 INRIA(法国国家研究机构)于 20 世纪 80 年代末开发。在 Xavier Leroy 的领导下,该语言于 1996 年左右演变为 Objective Caml (OCaml);修订后的语言添加了模块和面向对象。许多人认为 OCaml 比 SML(另一种主要的 ML 方言)“更实用”,因此在工业界得到广泛应用。另请参阅 F#。在线资源请访问caml.inria.fr/

Caml and OCaml Caml is a dialect of ML developed by Guy Cousineau and colleagues at INRIA (the French National research institute) beginning in the late 1980s. Evolved into Objective Caml (OCaml) around 1996, under the leadership of Xavier Leroy; the revised language adds modules and object orientation. Regarded by many as “more practical” than SML (the other principal ML dialect), OCaml is widely used in industry. See also F#. On-line resources at caml.inria.fr/.

雪松 参见台地和雪松。

Cedar See Mesa and Cedar.

Cilk 麻省理工学院的 Charles Leiserson 及其同事于 20 世纪 90 年代中期开始开发 C 和 C++ 的并发扩展,并于 2006 年由 Cilk Arts, Inc. 商业化;2009 年被英特尔收购。C 的扩展刻意被最小化:函数可以作为单独的任务生成;可以使用sync等待子任务的完成;任务可以在有限的程度上通过intake相互同步。实现采用一种新颖、可证明有效的工作窃取调度程序。在线资源位于supertech.csail.mit.edu/cilk//software.intel.com/en-us/intel-cilk-plus。

Cilk Concurrent extension of C and C++ developed by Charles Leiserson and associates at MIT beginning in the mid 1990s, and commercialized by Cilk Arts, Inc. in 2006; acquired by Intel in 2009. Extensions to C are deliberately minimal: a function can be spawned as a separate task; completion of subtasks can be awaited en masse with sync; tasks can synchronize with each other to a limited degree with inlets. Implementation employs a novel, provably efficient work-stealing scheduler. On-line resources at supertech.csail.mit.edu/cilk/ and /software.intel.com/en-us/intel-cilk-plus.

CLOS  Common Lisp 对象系统 [ Kee89 Sei05,第 16-17 章]。一组面向对象的 Common Lisp 扩展,已纳入 ANSI 标准语言(请参阅 Common Lisp)。

CLOS The Common Lisp Object System [Kee89; Sei05, Chaps. 16–17]. A set of object-oriented extensions to Common Lisp, incorporated into the ANSI standard language (see Common Lisp).

Clu 由 Barbara Liskov 及其同事于 20 世纪 70 年代末在麻省理工学院开发 [ LG86 ]。旨在提供一组异常强大的数据抽象功能 [ LSAS77 ]。还包括迭代器和异常处理。文档和免费提供的实现位于pmg.csail.mit.edu/CLU.html。

Clu Developed by Barbara Liskov and associates at MIT in the late 1970s [LG86]. Designed to provide an unusually powerful set of features for data abstraction [LSAS77]. Also includes iterators and exception handling. Documentation and freely available implementations at pmg.csail.mit.edu/CLU.html.

Cobol 最初由美国国防部于 20 世纪 50 年代末和 60 年代初由 Grace Murray Hopper 领导的团队开发。长期以来,它是世界上使用最广泛的编程语言。1968 年由 ANSI 标准化;1974 年和 1985 年修订。主要用于业务数据处理。引入了结构概念。精心设计的 I/O 设施。Cobol 2002 和 2014 [ Int14a ] 添加了各种现代语言功能,包括面向对象。

Cobol Originally developed by the U.S. Department of Defense in the late 1950s and early 1960s by a team led by Grace Murray Hopper. Long the most widely used programming language in the world. Standardized by ANSI in 1968; revised in 1974 and 1985. Intended principally for business data processing. Introduced the concept of structures. Elaborate I/O facilities. Cobol 2002 and 2014 [Int14a] add a variety of modern language features, including object orientation.

Common Lisp 大型、广泛使用的 Lisp 方言(另请参阅 Lisp)。包括(除其他外)静态作用域、广泛的类型系统、异常处理和面向对象功能(请参阅 CLOS)。多年来,标准参考是 Guy Steele, Jr. 的书 [ Ste90 ]。随后由 ANSI [ Ame96b ]标准化;Seibel [ Sei05 ]的最新文本,标准的精简超文本版本可在lispworks.com/documentation/HyperSpec/Front/index.htm 上找到。

Common Lisp Large, widely used dialect of Lisp (see also Lisp). Includes (among other things) static scoping, an extensive type system, exception handling, and object-oriented features (see CLOS). For years the standard reference was the book by Guy Steele, Jr. [Ste90]. Subsequently standardized by ANSI [Ame96b]; recent text by Seibel [Sei05] Abridged hypertext version of the standard available at lispworks.com/documentation/HyperSpec/Front/index.htm.

CSP 参见奥卡姆。

CSP See Occam.

埃菲尔 一种面向对象语言,由 Bertrand Meyer 及其巴黎逻辑工具学会的同事开发 [ Mey92bECM06b ]。包括(除其他功能外)多重继承、自动垃圾收集和强大的机制,用于重命名派生类中的数据成员和方法。在线资源位于eiffel.com/

Eiffel An object-oriented language developed by Bertrand Meyer and associates at the Société des Outils du Logiciel à Paris [Mey92b, ECM06b]. Includes (among other things) multiple inheritance, automatic garbage collection, and powerful mechanisms for renaming of data members and methods in derived classes. On-line resources at eiffel.com/.

Erlang 一种函数式语言,广泛支持分布式、容错和消息传递。由 Joe Armstrong 及其同事于 20 世纪 80 年代末在爱立信计算机科学实验室开发 [ Arm13 ]。自 1998 年起作为开源软件发布。用于实现爱立信和其他公司的各种产品,尤其是欧洲电信行业。在线资源请访问erlang.org/

Erlang A functional language with extensive support for distribution, fault tolerance, and message passing. Developed by Joe Armstrong and colleagues at Ericsson Computer Science Laboratory starting in the late 1980s [Arm13]. Distributed as open source since 1998. Used to implement a variety of products from Ericsson and other companies, particularly in the European telecom industry. On-line resources at erlang.org/.

Euclid 命令式语言,由 Butler Lampson 及其同事于 20 世纪 70 年代中期在施乐帕洛阿尔托研究中心开发 [ LHL + 77 ]。旨在消除 Pascal 中许多常见编程错误的来源,并促进程序的形式化验证。具有封闭的作用域和模块类型。

Euclid Imperative language developed by Butler Lampson and associates at the Xerox Palo Alto Research Center in the mid-1970s [LHL+77]. Designed to eliminate many of the sources of common programming errors in Pascal, and to facilitate formal verification of programs. Has closed scopes and module types.

F# 微软研究院的 Don Syme 及其同事开发的 OCaml 的后代。首次公开发布于 2005 年 [ SGC13 ]。与 OCaml 的区别主要在于它能够与 .NET 框架集成。在线资源位于research.microsoft.com/fsharp/msdn.microsoft.com/en-us/library/dd233154.aspx。

F# A descendant of OCaml developed by Don Syme and colleagues at Microsoft Research. First public release was in 2005 [SGC13]. Differences from OCaml are primarily to accommodate integration with the .NET framework. On-line resources at research.microsoft.com/fsharp/ and msdn.microsoft.com/en-us/library/dd233154.aspx.

Forth 一门小型而精巧的基于堆栈的语言,专为资源有限的机器解释而设计 [ Bro87 Int97 ]。最初由 Charles H. Moore 于 20 世纪 60 年代末开发。在仪器仪表和过程控制社区拥有一批忠实的追随者。

Forth A small and rather ingenious stack-based language designed for interpretation on machines with limited resources [Bro87, Int97]. Originally developed by Charles H. Moore in the late 1960s. Has a loyal following in the instrumentation and process-control communities.

Fortran 最初的高级命令式语言。由 IBM 的 John Backus 及其同事于 20 世纪 50 年代中期开发。重要的历史版本包括 Fortran I、Fortran II、Fortran IV、Fortran 77 和 Fortran 90。后两个版本记录在一对 ANSI 标准中。Fortran 90 [ MR96 ](1995 年更新)是对该语言的一次重大修订,添加了(除其他内容外)递归、指针、新控制结构和大量数组操作。Fortran 2003 添加了面向对象。Fortran 2008 [ Int10 ](2010 年批准)添加了几个泛型、联合数组(用于分布式内存计算机的具有明确额外“位置”维度的数组)以及用于迭代独立的循环的DO CONCURRENT结构。Fortran 77 继续被广泛使用。免费提供的gfortran实现符合所有现代标准,并作为gcc编译器套件 ( gnu.org/software/gcc/fortran/ ) 的一部分进行分发。从gcc版本 3.4开始,不再支持较旧的g77前端。

Fortran The original high-level imperative language. Developed in the mid-1950s by John Backus and associates at IBM. Important historical versions include Fortran I, Fortran II, Fortran IV, Fortran 77, and Fortran 90. The latter two were documented in a pair of ANSI standards. Fortran 90 [MR96] (updated in 1995) was a major revision to the language, adding (among other things) recursion, pointers, new control constructs, and a wealth of array operations. Fortran 2003 adds object orientation. Fortran 2008 [Int10] (approved in 2010) adds several generics, co-arrays (arrays with an explicit extra “location” dimension for distributed-memory machines), and a DO CONCURRENT construct for loops whose iterations are independent. Fortran 77 continues to be widely used. Freely available gfortran implementation conforms to all modern standards, and is distributed as part of the gcc compiler suite (gnu.org/software/gcc/fortran/). Support for the older g77 front end was discontinued as of gcc version 3.4.

Go 是 一种静态类型语言,由 Google 的 Robert Griesemer、Rob Pike、Ken Thompson 及其同事于 2007 年开始开发。旨在将脚本语言的简单性与编译的效率结合起来,部分原因是人们认为 C++ 已经变得过于庞大和复杂。包括垃圾收集、强类型、本地类型推断、类型扩展和接口(作为类的替代)、可变长度和关联数组以及基于消息的并发。在 Google 内部和外部均积极使用。在线资源位于golang.org/

Go A statically typed language developed by Robert Griesemer, Rob Pike, Ken Thompson, and colleagues at Google, beginning in 2007. Intended to combine the simplicity of scripting languages with the efficiency of compilation, and motivated in part by the perception that C++ had grown too large and complicated. Includes garbage collection, strong typing, local type inference, type extension and interfaces as an alternative to classes, variable-length and associative arrays, and message-based concurrency. In active use both within and outside Google. Online resources at golang.org/.

Haskell 领先的纯函数式语言。源自 Miranda。由一个研究委员会于 1987 年开始设计。包括柯里化函数、高阶函数、非严格语义、静态多态类型、模式匹配、列表推导、模块、单子 I/O、类型类和基于布局(缩进)的语法分组。Haskell 98 多年来一直是标准;已被 Haskell 2010 [ PJ10 ] 取代。在线资源位于haskell.org/。还设计了几种并发变体,包括 Concurrent Haskell [ JGF96 ] 和 pH [ NA01 ]。

Haskell The leading purely functional language. Descended from Miranda. Designed by a committee of researchers beginning in 1987. Includes curried functions, higher order functions, nonstrict semantics, static polymorphic typing, pattern matching, list comprehensions, modules, monadic I/O, type classes, and layout (indentation)-based syntactic grouping. Haskell 98 was for many years the standard; superceded by Haskell 2010 [PJ10]. On-line resources at haskell.org/. Several concurrent variants have also been devised, including Concurrent Haskell [JGF96] and pH [NA01].

Icon  Snobol 的后继者。由亚利桑那大学的 Ralph Griswold(Snobol 的首席设计师)开发 [ GG96 ]。采用更传统的控制流结构,但具有基于模式匹配和回溯的强大迭代和搜索功能。在线资源位于cs.arizona.edu/icon/。

Icon The successor to Snobol. Developed by Ralph Griswold (Snobol's principal designer) at the University of Arizona [GG96]. Adopts more conventional control-flow constructs, but with powerful iteration and search facilities based on pattern matching and backtracking. On-line resources at cs.arizona.edu/icon/.

Java 一种面向对象语言,主要基于 C++ 的一个子集。由 Sun Microsystems 的 James Gosling 及其同事于 20 世纪 90 年代初开发 [ AG06 GJS + 14 ]。旨在构建高度可移植、与体系结构无关的程序。与用于在 Java虚拟机上执行的中间字节码格式一起定义[ LYBB14 ]。包括(除其他内容外)变量(类类型)参考模型、混合继承、线程以及用于图形、通信和其他活动的大量预定义库。在线资源位于docs.oracle.com/javase/。

Java Object-oriented language based largely on a subset of C++. Developed by James Gosling and associates at Sun Microsystems in the early 1990s [AG06, GJS+14]. Intended for the construction of highly portable, architecture-neutral programs. Defined in conjunction with an intermediate bytecode format intended for execution on a Java virtual machine [LYBB14]. Includes (among other things) a reference model of (class-typed) variables, mix-in inheritance, threads, and extensive predefined libraries for graphics, communication, and other activities. On-line resources at docs.oracle.com/javase/.

JavaScript 由 Netscape Corp. 的 Brendan Eich 于 20 世纪 90 年代中期开发的简单脚本语言,用于客户端 Web 脚本编写。除了表面上的语法相似性之外,与 Java 没有任何联系。嵌入在大多数商业 Web 浏览器中。Microsoft 的 JScript 非常相似。两者于 1997 年合并为单一 ECMA 标准;随后由 ISO [ ECM11 ] 修订并交叉标准化。

JavaScript Simple scripting language developed by Brendan Eich at Netscape Corp. in the mid 1990s for the purpose of client-side web scripting. Has no connection to Java beyond superficial syntactic similarity. Embedded in most commercial web browsers. Microsoft's JScript is very similar. The two were merged into a single ECMA standard in 1997; subsequently revised and cross-standardized by the ISO [ECM11].

Lisp 最初的函数式语言 [ McC60 ]。由 John McCarthy 于 20 世纪 50 年代末开发,是 Church 的 lambda 演算的实现。存在许多方言。目前最常见的两种是 Common Lisp 和 Scheme(参见单独的条目)。历史上重要的方言包括 Lisp 1.5 [ MAE + 65 ]、MacLisp [ Moo78 ] 和 Interlisp [ TM81 ]。

Lisp The original functional language [McC60]. Developed by John McCarthy in the late 1950s as a realization of Church's lambda calculus. Many dialects exist. The two most common today are Common Lisp and Scheme (see separate entries). Historically important dialects include Lisp 1.5 [MAE+65], MacLisp [Moo78], and Interlisp [TM81].

Lua 轻量级脚本语言,主要用于扩展/嵌入设置。最初由里约热内卢天主教大学的 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 开发。旨在简单、快速、易于扩展和移植到新环境。标准实现也相当小——解释器和所有标准库都远低于 1 MB。在游戏行业以及其他各种领域。在线资源请访问lua.org/

Lua Lightweight scripting language intended primarily for extension/embedded settings. Originally developed by Roberto Ierusalimschy, Waldemar Celes, and Luiz Henrique de Figueiredo at the Pontifical Catholic University of Rio de Janeiro. Intended to be simple, fast, and easy to extend and to port to new environments. The standard implementation is also quite small—well under 1 MB for the interpreter and all the standard libraries. Heavily used in the gaming industry and in a wide variety of other fields. Online resources at lua.org/.

Mesa 和 Cedar  Mesa [ LR80 ] 是 Euclid 的后继者,由 Butler Lampson 领导的团队于 20 世纪 70 年代在施乐公司的帕洛阿尔托研究中心开发。包括基于监视器的并发性。与 Interlisp 和 Smalltalk 一起,是率先使用个人工作站的三个同伴项目之一,具有位图显示、鼠标和图形用户界面。Cedar [ SZBH86 ] 是 Mesa 的后继者,具有(除其他外)完整的类型安全、异常和自动垃圾收集功能。

Mesa and Cedar Mesa [LR80] was a successor to Euclid developed in the 1970s at Xerox's Palo Alto Research Center by a team led by Butler Lampson. Includes monitor-based concurrency. Along with Interlisp and Smalltalk, one of three companion projects that pioneered the use of personal workstations, with bitmapped displays, mice, and a graphical user interface. Cedar [SZBH86] was a successor to Mesa with (among other things) complete type safety, exceptions, and automatic garbage collection.

Miranda 由 David Turner 在 20 世纪 80 年代中期设计的纯函数式语言 [ Tur86 ]。源自 ML;具有类型推断和自动柯里化功能。添加了列表推导 (第 8.6 节),并对所有参数使用惰性求值。使用缩进和换行符进行语法分组。在线资源位于miranda.org.uk/

Miranda Purely functional language designed by David Turner in the mid-1980s [Tur86]. Descended from ML; has type inference and automatic currying. Adds list comprehensions (Section 8.6), and uses lazy evaluation for all arguments. Uses indentation and line breaks for syntactic grouping. On-line resources at miranda.org.uk/.

ML 具有“类似 Pascal”语法的函数式语言。最初由爱丁堡大学的 Robin Milner 及其同事于 20 世纪 70 年代中后期设计,作为程序验证系统的元语言(因此得名)。率先实现了积极的编译时类型推断和多态性。具有一些必要的特性。存在几种方言;最广泛使用的有标准 ML [ MTHM97 ] 和 OCaml(参见单独条目)。新泽西标准 ML 是普林斯顿大学和贝尔实验室的一个项目,已为许多平台生成了免费可用的实现( smlnj.org/)。

ML Functional language with “Pascal-like” syntax. Originally designed in the mid- to late 1970s by Robin Milner and associates at the University of Edinburgh as the meta-language (hence the name) for a program verification system. Pioneered aggressive compile-time type inference and polymorphism. Has a few imperative features. Several dialects exist; the most widely used are Standard ML [MTHM97] and OCaml (see separate entry). Standard ML of New Jersey, a project of Princeton University and Bell Labs, has produced freely available implementations for many platforms (smlnj.org/).

Modula 和 Modula-2 是 Pascal 的直接后继者,由 Niklaus Wirth 开发。最初的 Modula [ Wir77b ] 是一种明确基于并发监视器的语言。有时它也被称为 Modula (1),以区别于它的后继者。更有影响力的 Modula-2 [ Wir85b ] 最初设计时就加入了协程(第 9.5 节),但没有真正的并发功能。这两种语言都提供了模块管理器式数据抽象机制。Modula-2 于 1996 年由 ISO 标准化 [ Int96 ]。可从nongnu.org/gm2/获取适用于多种平台的免费实现。

Modula and Modula-2 The immediate successors to Pascal, developed by Niklaus Wirth. The original Modula [Wir77b] was an explicitly concurrent monitor-based language. It is sometimes called Modula (1) to distinguish it from its successors. The more influential Modula-2 [Wir85b] was originally designed with coroutines (Section 9.5), but no real concurrency. Both languages provide mechanisms for module-as-manager style data abstractions. Modula-2 was standardized by the ISO in 1996 [Int96]. Freely available implementation for several platforms available from nongnu.org/gm2/.

Modula-3 是 Modula-2 的一个主要扩展,由 Luca Cardelli、Jim Donahue、Mick Jordan、Bill Kalsow 和 Greg Nelson 在 20 世纪 80 年代末在数字系统研究中心和 Olivetti 研究中心开发 [ Har92 ]。旨在提供与 Ada 相当的对大型、可靠和可维护系统的支持,但形式更简单、更优雅。在线资源请访问modula3.org/。

Modula-3 A major extension to Modula-2 developed by Luca Cardelli, Jim Donahue, Mick Jordan, Bill Kalsow, and Greg Nelson at the Digital Systems Research Center and the Olivetti Research Center in the late 1980s [Har92]. Intended to provide a level of support for large, reliable, and maintainable systems comparable to that of Ada, but in a simpler and more elegant form. On-line resources at modula3.org/.

Oberon  Niklaus Wirth 设计的一种刻意简化的语言 [ Wir88b , RW92 ]。本质上是 Modula-2 [ Wir88a ]的子集,增加了类型扩展机制 (第 10.2.4 节) [ Wir88c ]。在线资源请访问oberon.ethz.ch/

Oberon A deliberately minimal language designed by Niklaus Wirth [Wir88b, RW92]. Essentially a subset of Modula-2 [Wir88a], augmented with a mechanism for type extension (Section 10.2.4) [Wir88c]. On-line resources at oberon.ethz.ch/.

Objective-C 基于 Smalltalk 风格“消息传递”的面向对象 C 语言扩展。由 Brad Cox 和 StepStone 公司于 20 世纪 80 年代初设计。NeXT Software, Inc. 于 20 世纪 80 年代末采用该语言,用于其NeXTStep 操作系统和编程环境。Apple 于 1997 年收购 NeXT 后,将其作为 Mac OS X 的主要开发语言。比其他面向对象的 C 语言后代简单得多。以完全动态的方法调度和不寻常的消息语法为特色。gcc发行版中包含免费提供的实现(参见 C)。在线文档可通过 developer.apple.com/library/mac/navigation 搜索找到。Apple未来的开发将转向 Swift(参见单独条目)。

Objective-C An object-oriented extension to C based on Smalltalk-style “messaging.” Designed by Brad Cox and StepStone corporation in the early 1980s. Adopted by NeXT Software, Inc., in the late 1980s for their NeXTStep operating system and programming environment. Adopted by Apple as the principal development language for Mac OS X after Apple acquired NeXT in 1997. Substantially simpler than other object-oriented descendants of C. Distinguished by fully dynamic method dispatch and unusual messaging syntax. Freely available implementation included in the gcc distribution (see C). On-line documentation can be found with a search at developer.apple.com/library/mac/navigation. Future development at Apple is moving to Swift (see separate entry).

OCaml 参见 Caml。

OCaml See Caml.

Occam 一种基于 CSP [ Hoa78 ] 的并发语言 [ JG89 ] ,Hoare 的表示法,用于使用受保护命令和同步发送进行基于消息的通信。该语言是 INMOS 公司的transputer处理器构建系统的首选语言,曾广泛用于欧洲。使用缩进和换行符进行句法分组。在线资源请访问wotug.org/occam/。

Occam A concurrent language [JG89] based on CSP [Hoa78], Hoare's notation for message-based communication using guarded commands and synchronization send. The language of choice for systems built from INMOS Corporation's transputer processors, once widely used in Europe. Uses indentation and line breaks for syntactic grouping. On-line resources at wotug.org/occam/.

Pascal 由 Niklaus Wirth 于 20 世纪 60 年代末设计 [ Wir71 ],主要是为了回应 Algol 68,后者被广泛认为过于臃肿。在 20 世纪 70 年代和 80 年代被广泛使用,尤其是在教学中。引入了子范围和枚举类型。统一了结构和联合。多年来,标准参考是 Wirth 与 Kathleen Jensen 合著的书 [ JW91 ];随后由 ISO 和 ANSI [ Int90 ] 标准化。免费提供的实现位于gnu-pascal.de/gpc/h-index.html。

Pascal Designed by Niklaus Wirth in the late 1960s [Wir71], largely in reaction to Algol 68, which was widely perceived as bloated. Heavily used in the 1970s and 1980s, particularly for teaching. Introduced subrange and enumeration types. Unified structures and unions. For many years, the standard reference was Wirth's book with Kathleen Jensen [JW91]; subsequently standardized by ISO and ANSI [Int90]. Freely available implementation available at gnu-pascal.de/gpc/h-index.html.

Perl 由 Larry Wall 在 20 世纪 80 年代末设计的一种通用脚本语言 [ CfWO12 ]。包括基于(扩展的)正则表达式的异常广泛的字符串操作和模式匹配机制。借用了 C、 sed、awk [ AKW88 ] 和各种 Unix shell(命令解释器)语言的功能。因能用多种方式完成几乎所有事情而闻名/臭名昭著。20 世纪 90 年代末,作为一种服务器端 Web 脚本语言,Perl 的受欢迎程度迅速飙升。第 5 版于 1995 年发布;截至 2015 年,第 6 版仍在开发中。在线资源请访问perl.org/

Perl A general-purpose scripting language designed by Larry Wall in the late 1980s [CfWO12]. Includes unusually extensive mechanisms for character string manipulation and pattern matching based on (extended) regular expressions. Borrows features from C, sed, awk [AKW88], and various Unix shell (command interpreter) languages. Is famous/infamous for having multiple ways of doing almost anything. Enjoyed an upsurge in popularity in the late 1990s as a server-side web scripting language. Version 5 released in 1995; version 6 still under development as of 2015. On-line resources at perl.org/.

PHP  Perl 的后代,设计用于服务器端 Web 脚本。脚本通常嵌入在网页中。最初由 Rasmus Lerdorf 于 1995 年创建,用于帮助管理其个人主页。现在,该名称正式采用递归缩写(PHP:超文本预处理器)。较新的版本由 Andi Gutmans 和 Zeev Suraski 与 Lerdorf 合作开发。包括对各种 Internet 协议的内置支持以及对数十种不同商业数据库系统的访问。版本 5(2004 年)添加了广泛的面向对象功能、混合继承、迭代器对象、自动加载、结构化异常处理、反射、重载和参数的可选类型声明。在线资源请访问php.net/。

PHP A descendant of Perl designed for server-side web scripting. Scripts are typically embedded in web pages. Originally created by Rasmus Lerdorf in 1995 to help manage his personal home page. The name is now officially a recursive acronym (PHP: Hypertext Preprocessor). More recent versions due to Andi Gutmans and Zeev Suraski, in cooperation with Lerdorf. Includes built-in support for a wide range of Internet protocols and for access to dozens of different commercial database systems. Version 5 (2004) added extensive object-oriented features, mix-in inheritance, iterator objects, autoloading, structured exception handling, reflection, overloading, and optional type declarations for parameters. On-line resources at php.net/.

PL/I 一种大型通用语言,设计于 20 世纪 60 年代中期,是 Fortran、Cobol 和 Algol 的后继者 [ Bee70 ]。从未取代过其前辈;主要通过 IBM 公司影响力而得以存活。

PL/I A large, general-purpose language designed in the mid-1960s as a successor to Fortran, Cobol, and Algol [Bee70]. Never managed to displace its predecessors; kept alive largely through IBM corporate influence.

后记 一种用于描述图形和打印操作的基于堆栈的语言 [ Ado90 ]。由 Adob​​e Systems, Inc. 开发和推广。部分基于 Forth [ Bro87 ]。由许多文字处理器和绘图程序生成。大多数专业级打印机都包含 Postscript 解释器。

Postscript A stack-based language for the description of graphics and print operations [Ado90]. Developed and marketed by Adobe Systems, Inc. Based in part on Forth [Bro87]. Generated by many word processors and drawing programs. Most professional-quality printers contain a Postscript interpreter.

Prolog 最广泛使用的逻辑编程语言。由法国艾克斯-马赛大学的 Alain Colmeraurer 和 Philippe Roussel 以及苏格兰爱丁堡大学的 Robert Kowalski 及其同事于 20 世纪 70 年代初开发。存在许多方言。1995 年部分标准化 [ Int95 ]。有许多实现,包括免费和商业实现;流行的免费版本包括 GNU Prolog ( gprolog.org/ ) 和 SWI-Prolog ( swi-prolog.org/ )。

Prolog The most widely used logic programming language. Developed in the early 1970s by Alain Colmeraurer and Philippe Roussel of the University of Aix–Marseille in France and Robert Kowalski and associates at the University of Edinburgh in Scotland. Many dialects exist. Partially standardized in 1995 [Int95]. Numerous implementations, both free and commercial, are available; popular freely available versions include GNU Prolog (gprolog.org/) and SWI-Prolog (swi-prolog.org/).

Python 一种通用的面向对象脚本语言,由 Guido van Rossum 于 20 世纪 90 年代初设计。使用缩进进行语法分组。包括动态类型、具有词法作用域的嵌套函数、lambda 表达式和高阶函数、真正的迭代器、列表推导、数组切片、反射、结构化异常处理、多重继承以及模块和动态加载。在线资源请访问python.org/。

Python A general-purpose, object-oriented scripting language designed by Guido van Rossum in the early 1990s. Uses indentation for syntactic grouping. Includes dynamic typing, nested functions with lexical scoping, lambda expressions and higher order functions, true iterators, list comprehensions, array slices, reflection, structured exception handling, multiple inheritance, and modules and dynamic loading. On-line resources at python.org/.

R 开源脚本语言,主要用于统计分析。基于专有的 S 统计编程语言,最初由贝尔实验室的 John Chambers 等人开发。支持一等和高阶函数、无限范围、按需调用、多维数组和切片以及丰富的统计函数库。在线资源请访问r-project.org/

R Open-source scripting language intended primarily for statistical analysis. Based on the proprietary S statistical programming language, originally developed by John Chambers and others at Bell Labs. Supports first-class and higher order functions, unlimited extent, call-by-need, multidimensional arrays and slices, and an extensive library of statistical functions. On-line resources at r-project.org/.

Ruby 一种优雅、通用、面向对象的脚本语言,由 Yukihiro “Matz” Matsumoto 于 1993 年开始设计。首次发布于 1995 年。受到 Ada、Eiffel 和 Perl 的启发,带有 Python、Lisp、Clu 和 Smalltalk 的痕迹。包括动态类型、任意精度算法、真正的迭代器、用户级线程、一等函数和高阶函数、延续、反射、Smalltalk 样式的消息传递、混合继承、自动加载、结构化异常处理以及对 Tk 窗口工具包的支持。Thomas 和 Hunt 撰写的文本是标准参考 [ TFH13 ]。在线资源位于ruby​​-lang.org/

Ruby An elegant, general-purpose, object-oriented scripting language designed by Yukihiro “Matz” Matsumoto, beginning in 1993. First released in 1995. Inspired by Ada, Eiffel, and Perl, with traces of Python, Lisp, Clu, and Smalltalk. Includes dynamic typing, arbitrary precision arithmetic, true iterators, user-level threads, first-class and higher order functions, continuations, reflection, Smalltalk-style messaging, mix-in inheritance, autoloading, structured exception handling, and support for the Tk windowing toolkit. The text by Thomas and Hunt is a standard reference [TFH13]. On-line resources at ruby-lang.org/.

Rust 一种用于系统编程的静态类型语言,最初由 Graydon Hoare 及其 Mozilla Research 的同事开发。语法上让人联想到 C,但更注重类型安全、内存安全(没有自动垃圾收集)和并发性。包括本地类型推断、类似 Haskell 的类型特征、泛型、混合继承、模式匹配和基于所有权转移的线程间通信。静态禁止空指针和悬空指针。在线资源请访问rust-lang.org/。

Rust A statically typed language for systems programming, initially developed by Graydon Hoare and colleagues at Mozilla Research. Syntactically reminiscent of C, but with a strong emphasis on type safety, memory safety (without automatic garbage collection), and concurrency. Includes local type inference, Haskell-like type traits, generics, mix-in inheritance, pattern matching, and inter-thread communication based on ownership transfer. Statically prohibits both null and dangling pointers. On-line resources at rust-lang.org/.

Scala 面向对象的函数式语言,由瑞士洛桑联邦理工学院的 Martin Odersky 及其同事于 2001 年开始开发。旨在在 Java 虚拟机之上实现,部分原因是 Java 中存在的缺陷。提供可以说是将函数式特性最积极地集成到命令式语言中的语言。具有一等函数和高阶函数、局部类型推断、惰性求值、模式匹配、柯里化、尾部递归、丰富的泛型(具有协变和逆变)、基于消息的并发、基于特征的继承(类和接口之间的某种交叉)。在工业界和学术界都得到了广泛使用。由欧洲研究委员会资助开发。在线资源位于scala-lang.org/

Scala Object-oriented functional language developed by Martin Odersky and associates at the École Polytechnique Fédérale de Lausanne in Switzerland, beginning in 2001. Intended for implementation on top of the Java Virtual Machine, and motivated in part by perceived shorcomings in Java. Provides arguably the most aggressive integration of functional features into an imperative language. Has first-class and higher order functions, local type inference, lazy evaluation, pattern matching, currying, tail recursion, rich generics (with covariance and contravariance), message-based concurrency, trait-based inheritance (sort of a cross between classes and interfaces). Heavily used in both industry and academia. Development funded by the European Research Council. On-line resources at scala-lang.org/.

Scheme 一种小巧、优雅的 Lisp (另请参阅 Lisp) 方言,由 Guy Steele 和 Gerald Sussman 于 20 世纪 70 年代中期开发。具有静态作用域和真正的一等函数。广泛用于教学。第六个修订标准 (R6RS) [ SDF + 07 ] 于 2007 年发布,大大增加了语言的大小;在随后的反对意见之后,R7RS 标准 [ SCG + 13 ] 编纂了一个类似于 R5RS 版本的小型核心语言和一组更大的扩展。早期版本由 IEEE 和 ANSI [ Ins91 ] 标准化。Abelson 和 Sussman 编写的书 [ AS96 ] 长期用于 MIT 和其他地方的入门编程课程,是基本编程概念的经典指南,尤其是函数式编程。在线资源请访问community.schemewiki.org/。

Scheme A small, elegant dialect of Lisp (see also Lisp) developed in the mid-1970s by Guy Steele and Gerald Sussman. Has static scoping and true first-class functions. Widely used for teaching. The sixth revised standard (R6RS) [SDF+07], released in 2007, substantially increased the size of the language; in the wake of subsequent objections, the R7RS standard [SCG+13] codifies a small core language similar to the R5RS version and a larger set of extensions. Earlier version standardized by the IEEE and ANSI [Ins91]. Thebook by Abelson and Sussman [AS96], long used for introductory programming classes at MIT and elsewhere, is a classic guide to fundamental programming concepts, and to functional programming in particular. On-line resources at community.schemewiki.org/.

Simula 由 Ole-Johan Dahl、Bjørn Myhrhaug 和 KristenNygaard 于 20 世纪 60 年代中期在奥斯陆挪威计算中心设计 [ BDMN73 ND78 ]。使用类协程扩展了 Algol 60。该语言的名称反映了其适用于离散事件模拟(第 C-9.5.4 节)。directory.fsf.org/project/cim/ 提供免费的 Simula-to-C 转换器

Simula Designed at the Norwegian Computing Center, Oslo, in the mid-1960s by Ole-Johan Dahl, Bjørn Myhrhaug, and KristenNygaard [BDMN73, ND78]. Extends Algol 60 with classes and coroutines. The name of the language reflects its suitability for discrete-event simulation (Section C-9.5.4). Free Simula-to-C translator available at directory.fsf.org/project/cim/.

单任务 C (SAC) 一种纯函数式语言,设计用于对基于数组的数据进行高性能计算 [ Sch03 ]。由 Sven-Bodo Scholz 及其同事于 1994 年开始在赫特福德大学和其他几所机构开发。与 Sisal 精神类似,但语法尽可能地基于 C。在线资源请访问 www.sac-home.org/。

Single Assignment C (SAC) A purely functional language designed for high performance computing on array-based data [Sch03]. Developed by Sven-Bodo Scholz and associates at University of Hertfordshire and several other institutions beginning in 1994. Similar in spirit to Sisal, but with syntax based as heavily as possible on C. On-line resources at www.sac-home.org/.

Sisal 一种具有“命令式”语法的函数式语言。由詹姆斯·麦格劳 (James McGraw) 及其同事在劳伦斯利弗莫尔国家实验室于 20 世纪 80 年代早期至中期开发 [ FCO90 Can92 ]。主要用于高性能科学计算,具有自动并行化功能。数据流语言 Val [ McG82 ]的后代。LLNL 不再开发该语言;可从 sisal.sourceforge.net/ 获取开源版本

Sisal A functional language with “imperative-style” syntax. Developed by James McGraw and associates at Lawrence Livermore National Laboratory in the early to mid-1980s [FCO90, Can92]. Intended primarily for high-performance scientific computing, with automatic parallelization. A descendant of the dataflow language Val [McG82]. No longer under development at LLNL; available open-source from sisal.sourceforge.net/.

Smalltalk 被许多人视为典型的面向对象语言。由 Alan Kay、Adele Goldberg、Dan Ingalls 及其同事在 20 世纪 70 年代在施乐帕洛阿尔托研究中心开发,最终形成了 Smalltalk-80 语言 [ GR89 ]。基于活动对象之间“消息”的拟人化编程模型。在线资源请访问smalltalk.org/。

Smalltalk Considered by many to be the quintessential object-oriented language. Developed by Alan Kay, Adele Goldberg, Dan Ingalls, and associates at the Xerox Palo Alto Research Center throughout the 1970s, culminating in the Smalltalk-80 language [GR89]. Anthropomorphic programming model based on “messages” between active objects. On-line resources at smalltalk.org/.

SML 参见 ML。

SML See ML.

Snobol 由贝尔实验室的 Ralph Griswold 及其同事于 20 世纪 60 年代开发 [ GPP71 ],最终发展为 SNOBOL4。主要用于处理字符串。包括一组极其丰富的字符串操作原语以及基于成功失败概念的新型控制流机制。在线档案位于snobol4.org/

Snobol Developed by Ralph Griswold and associates at Bell Labs in the 1960s [GPP71], culminating in SNOBOL4. Intended primarily for processing character strings. Included an extremely rich set of string-manipulating primitives and a novel control-flow mechanism based on the notions of success and failure. On-line archive at snobol4.org/.

SR 并发编程语言,由亚利桑那大学的 Greg Andrews 及其同事于 20 世纪 80 年代开发 [ AO93 ]。不仅集成了顺序和并发编程,还将共享内存、信号量、消息传递、远程过程和集合点集成到单一概念框架和简单语法中。在线存档位于cs.arizona.edu/sr/。

SR Concurrent programming language developed by Greg Andrews and colleagues at the University of Arizona in the 1980s [AO93]. Integrated not only sequential and concurrent programming but also shared memory, semaphores, message passing, remote procedures, and rendezvous into a single conceptual framework and simple syntax. On-line archive at cs.arizona.edu/sr/.

Swift 苹果公司开发的动态面向对象语言,是 Objective-C 的后继者。包括垃圾收集、局部类型推断、数组边界检查、关联数组、元组、具有无限范围的第一类 lambda 表达式、泛型以及对象类型的值和引用变量。在线资源请访问developer.apple.com/swift/。

Swift Dynamic object-oriented language developed by Apple as a successor to Objective-C. Includes garbage collection, local type inference, array bounds checking, associative arrays, tuples, first class lambda expressions with unlimited extent, generics, and both value and reference variables of object type. On-line resources at developer.apple.com/swift/.

Tcl/Tk 工具命令语言(发音为“tickle”)。由 John Ousterhout 于 20 世纪 80 年代末设计的脚本语言 [ Ous94 WJH03 ]。基于关键字的语法类似于 Unix 命令行调用和开关;标点符号相对较少。使用动态作用域。支持反射、解释器的递归调用。Tk(发音为“tee-kay”)是一组用于图形用户界面 (GUI) 编程的 Tcl 命令。Tk 由 Ousterhout 设计,作为 Tcl 的扩展,还嵌入了 Ruby、Perl 和其他几种语言。在线资源请访问tcl.tk/。

Tcl/Tk Tool command language (pronounced “tickle”). Scripting language designed by John Ousterhout in the late 1980s [Ous94, WJH03]. Keyword-based syntax resembles Unix command-line invocations and switches; punctuation is relatively spare. Uses dynamic scoping. Supports reflection, recursive invocation of interpreter. Tk (pronounced “tee-kay”) is a set of Tcl commands for graphical user interface (GUI) programming. Designed by Ousterhout as an extension to Tcl, Tk has also been embedded in Ruby, Perl, and several other languages. On-line resources at tcl.tk/.

Turing  20 世纪 80 年代初,多伦多大学的 Richard Holt 及其同事从 Euclid 中衍生而来 [ HMRC88 ]。最初旨在作为一种教学语言,但可用于广泛的应用。其衍生版本也由 Holt 的团队开发,包括 Turing Plus 和面向对象的 Turing。

Turing Derived from Euclid by Richard Holt and associates at the University of Toronto in the early 1980s [HMRC88]. Originally intended as a pedagogical language, but could be used for a wide range of applications. Descendants, also developed by Holt's group, include Turing Plus and Object-Oriented Turing.

XSL 可扩展样式表语言,由万维网联盟制定标准。用作 XML(可扩展标记语言)的标准样式表语言,XML 是自描述树形结构数据的标准,XHTML 是 XML 的一种方言。包括三个子标准:XSLT(XSL 转换)[ Wor14 ],指定如何将一种 XML 方言转换为另一种方言;XPath [ Wor07 ],用于命名 XML 文档的元素;XSL-FO(XSL 格式化对象)[ Wor06b ],指定如何格式化文档。XSLT 虽然专门用于 XML 转换,但它是一种图灵完备的编程语言 [ Kep04 ]。标准和其他资源请访问w3.org/Style/XSL/。

XSL Extensible Stylesheet Language, standardized by the World Wide Web Consortium. Serves as the standard stylesheet language for XML (Extensible Markup Language), the standard for self-descriptive tree-structured data, of which XHTML is a dialect. Includes three substandards: XSLT (XSL Transformations) [Wor14], which specifies how to translate from one dialect of XML to another; XPath [Wor07], used to name elements of an XML document; and XSL-FO (XSL Formatting Objects) [Wor06b], which specifies how to format documents. XSLT, though highly specialized to the transformation of XML, is a Turing complete programming language [Kep04]. Standards and additional resources at w3.org/Style/XSL/.

语言设计和语言实现

Language Design and Language Implementation

在本文中,我们有机会评论了语言设计和语言实现之间的许多联系。一些更直接的联系已在单独的侧边栏中突出显示。我们在这里列出了这些侧边栏。

Throughout this text, we have had occasion to remark on the many connections between language design and language implementation. Some of the more direct connections have been highlighted in separate sidebars. We list those sidebars here.

第 1 章:简介
1.1 介绍 10
1.2 编译型语言和解释型语言 18
1.3 Pascal 的早期成功 22
1.4 强大的开发环境 二十五
第 2 章:编程语言语法
2.1 上下文关键词 四十六
2.2 格式限制 四十八
2.3 嵌套注释 55
2.4 识别多种 token 63
2.5 最长的 token 64
2.6 悬而未决的else 81
2.7 递归下降和表驱动LL解析 86
第 3 章:名称、范围和绑定
3.1 绑定时间 117
3.2 Fortran 中的递归 119
3.3 相互递归 131
3.4 重新申报 134
3.5 模块和单独编译 140
3.6 动态作用域 143
3.7 C 和 Fortran 中的指针 146
3.8 OCaml 中的用户定义运算符 149
3.9 约束规则和范围 156
3.10 函数和函数对象 161
3.11 泛型作为宏 163
3.12 单独编译 41号
第四章:语义分析
4.1 动态语义检查 182
4.2 前向引用 193
4.3 属性赋值器 198
第五章:目标机架构
5.2 处理器/内存差距 62号
5.3 一兆字节是多少? 66号
5.4 延迟分支指令 碳 90
5.5 延迟加载说明 92号
5.6 内联子程序 97号
5.1 伪汇编符号 218
第 6 章:控制流
6.1 实现参考模型 232
6.2 安全与性能 241
6.3 评估顺序 242
6.4 清理延续 250
6.5 短路评估 255
6.6 案例陈述 259
6.7 数值不精确 264
6.8 for循环 267
6.9 “真正的”迭代器和迭代器对象 270
6.10 正序求值 282
6.11 不确定性和公平性 114号
第 7 章:类型系统
7.1 系统编程 299
7.2 动态类型 300
7.3 多语言字符集 306
7.4 十进制类型 307
7.5 整数的多种大小 309
7.6 非转换类型转换 319
7.7 Haskell 中重载函数的类型类 329
7.8 统一 331
7.9 机器学习中的泛型 334
7.10 重载和多态 336
7.11 为什么要擦除? 127号
第 8 章:复合类型
8.1 C 和 C++ 中的结构标签和 typedef 353
8.2 记录字段的顺序 356
8.13 变体字段的放置 141号
8.3 []是运算符吗? 361
8.4 数组布局 370
8.5 数组索引的下限 373
8.6 指针的实现 378
8.7 堆栈粉碎 385
8.8 指针和数组 386
8.9 垃圾收集 390
8.10 垃圾到底是什么? 393
8.11 引用计数与跟踪 396
8.12 汽车cdr 399
第 9 章:子程序和控制抽象
9.8 词汇嵌套和显示 164号
9.9 利用pc = r15 169号
9.10 在堆栈中执行代码 176号
9.1 提示和指示 420
9.2 内联和模块化 421
9.3 参数模式 424
9.11 按姓名呼叫 181号
9.12 需要的话请来电 182号
9.4 结构化异常 446
9.5 设置跳转 449
9.6 线程和协同程序 452
9.7 协程堆栈 453
第 10 章:数据抽象和面向对象
10.1 类声明中包括什么? 476
10.2 容器/收藏品 484
10.3 价值/参考权衡 498
10.4 初始化和赋值 501
10.5 “扩展”对象的初始化 502
10.6 反向赋值 511
10.7 脆弱基类问题 512
10.8 多重继承的成本 197号
第 11 章:函数式语言
11.1 函数式程序中的迭代 546
11.2 SML 和 Haskell 中的相等性和排序 554
11.3 OCaml 中的类型等价 560
11.4 惰性求值 570
11.5 单子 575
11.6 高阶函数 577
11.7 副作用和编译 582
第 12 章:逻辑语言
12.1 同像语言 608
12.2 反射 611
12.3 实现逻辑 613
12.4 替代搜索策略 614
第 13 章:并发
13.1 处理器到底是什么? 632
13.2 硬件和软件通信 636
13.3 任务并行和数据并行计算 643
13.4 违反直觉的实现 646
13.5 监视信号语义 672
13.6 嵌套监控问题 673
13.7 条件临界区 674
13.8 Java 中的条件变量 677
13.9 无副作用和隐式同步 685
13.10 实现问题的语义影响 241号
13.11 仿真与效率 243号
13.12 远程过程的参数 C 250
第 14 章:脚本语言
14.1 编译解释型语言 702
14.2 规范实现 703
14.3 shell 中的内置命令 707
14.4 神奇数字 712
14.5 JavaScript 和 Java 736
14.6 你能在多大程度上信任脚本? 737
14.7 W3C 和 WHATWG 260 号
14.8 关于动态作用域的思考 742
14.9 grep命令和 Unix 工具的诞生 744
14.10 正则表达式的自动机 746
14.11 编译正则表达式 749
14.12 Perl 中的类型团 754
14.13 可执行类声明 763
14.14 越糟越好 764
第 15 章:构建可运行程序
15.1 后记 783
15.2 单独编译的类型检查 799
第 16 章:运行时程序管理
16.1 运行时系统 808
16.2 优化基于堆栈的 IF 812
16.3 类文件和字节码的验证 820
16.4 假设有一个即时编译器 287号
16.5 引用和指针 290 号
16.6 模拟和解释 830
16.7 通过二进制重写创建沙箱 835
16.8 矮人 846
第 17 章:代码改进
17.1 窥孔优化 C 303
17.2 基本块 304 号
17.3 常见子表达式 309号
17.4 指针分析 C 310
17.5 循环不变量 324 号
17.6 控制流分析 325 号

t0010_at0010_bt0010_c

编号示例

Numbered Examples

第 1 章:简介
1.1x86 机器语言的 GCD 程序5
1.2x86 汇编程序中的 GCD 程序5
语言设计的艺术
编程语言范围
1.3编程语言的分类11
1.4C 语言中的 GCD 函数十三
1.5OCaml 中的 GCD 函数十三
1.6Prolog中的GCD规则十三
为什么要学习编程语言?
编译和解释
1.7纯编译17
1.8纯粹的解释17
1.9混合编译和解释18
1.10预处理19
1.11库例程和链接19
1.12编译后汇编20
1.13C 预处理器20
1.14源到源翻译20
1.15引导21
1.16编译解释型语言23
1.17动态和即时编译23
1.18微码(固件)24
编程环境
编译概述
1.19编译和解释阶段二十六
1.20C 语言中的 GCD 程序二十八
1.21GCD 程序代币二十八
1.22上下文无关语法和解析二十八
1.23GCD 程序解析树二十九
1.24GCD 程序抽象语法树33
1.25解释语法树33
1.26GCD程序汇编代码三十四
1.27GCD程序优化三十六
第 2 章:编程语言语法
2.1阿拉伯数字的语法43
指定语法:正则表达式和上下文无关文法
2.2C11 的词汇结构四十五
2.3数字常量的语法四十六
2.4表达式中的语法嵌套四十八
2.5扩展 BNF(EBNF)49
2.6斜率 * x + 截距的推导50
2.7解析斜率 * x + 截距的树51
2.8具有优先级和结合性的表达式语法52
扫描
2.9计算器语言的标记54
2.10计算器令牌的临时扫描器54
2.11计算器扫描仪的有限自动机55
2.12为给定的正则表达式构建 NFA58
2.13d * (. d | d . ) d * 的NFA59
2.14d * ( . d | d . ) d *的 DFA60
2.15d *( . d | d . ) d *的最小 DFA60
2.16嵌套case语句自动机62
2.17非平凡前缀问题64
2.18Fortran 扫描中的前瞻64
2.19表格驱动扫描65
解析
2.20自上而下和自下而上的解析70
2.21使用自下而上的语法来限制空间72
2.22自上而下的计算器语言语法73
2.23计算器语言的递归下降解析器75
2.24“求和与平均”程序的递归下降解析75
2.25左递归79
2.26常见前缀79
2.27消除左递归80
2.28左分解80
2.29解析“悬垂 else”80
2.30“悬而未决”程序错误81
2.31结构化语句的结束标记81
2.32elsif的必要性82
2.33自上而下解析的驱动程序和表82
2.34“求和与平均”程序的表驱动解析83
2.35计算器语言的预测集84
2.36导出id列表90
2.37计算器语言的自下而上的语法90
2.38自下而上解析“求和与平均”程序91
2.39自下而上的计算器语法的 CFSM95
2.40自下而上的计算器语法中的 Epsilon 产生式95
2.41带有 epsilon 生成的 CFSM101
2.42C 中的语法错误102
2.43C 中的语法错误(重复)1 号
2.44恐慌模式的问题1 号
2.45递归下降中的短语级恢复碳2
2.46级联语法错误碳3
2.47通过特定上下文的前瞻减少级联错误碳4
2.48具有完整短语级恢复的递归下降碳4
2.49递归下降解析器中的异常碳5
2.50“ ; else “的错误产生碳6
2.51FMQ 中的仅插入修复碳8
2.52有删除内容的 FMQ碳8
2.53yacc/bison中的恐慌模式11号
2.54带有语句终止符的恐慌模式11号
2.55yacc/bison中的短语级恢复11号
理论基础
2.56d * ( . d | d . ) d *的正式 DFA14号
2.57为十进制字符串 DFA 重建正则表达式15号
2.58具有大型最小 DFA 的正则语言16号
2.59指数 DFA 爆炸16号
2.600 n 1 n不是常规语言19号
2.61语法分类碳 20
2.62语言课程分离碳 20
第 3 章:名称、范围和绑定
绑定时间的概念
对象生命周期和存储管理
3.1局部变量的静态分配119
3.2运行时堆栈的布局120
3.3堆中的外部碎片122
范围规则
3.4C 中的静态变量127
3.5嵌套作用域128
3.6静态链130
3.7在声明前使用中的“陷阱”131
3.8C# 中的整个块范围132
3.9Python 中的“本地化”132
3.10Scheme 中的声明顺序132
3.11C 语言中的声明与定义133
3.12C 中的内部声明134
3.13伪随机数作为模块的动机136
3.14C++ 中的伪随机数生成器137
3.15模块作为类型的“管理器”138
3.16伪随机数生成器类型140
3.17大型应用程序中的模块和类142
3.18静态与动态作用域143
3.19具有动态作用域的运行时错误144
实现范围
3.45LeBlanc-Cook 符号表27号
3.46示例程序的符号表28号
3.47Lisp 中的 A 列表查找31号
3.48中央参考表33号
3.49顶级酒店倒闭33号
范围内名称的含义
3.20使用参数进行别名146
3.21别名和代码改进146
3.22Ada 中的重载枚举常量147
3.23解决歧义重载147
3.24C++ 中的重载148
3.25Ada 中的运算符重载148
3.26C++ 中的运算符重载148
3.27Haskell 中的中缀运算符149
3.28使用类型类进行重载149
3.29打印多种类型的对象150
引用环境的绑定
3.30深浅绑定152
3.31具有静态作用域的绑定规则154
3.32在 Scheme 中返回一等子程序156
3.33Java 中的对象闭包157
3.34C# 中的委托158
3.35代表和无限范围158
3.36C++ 中的函数对象158
3.37C# 中的 lambda 表达式159
3.38多种 lambda 语法159
3.39C++11 中的简单 lambda 表达式160
3.40C++ lambda 表达式中的变量捕获161
3.41Java 8 中的 Lambda 表达式162
宏扩展
3.42一个简单的汇编宏162
3.43C 中的预处理器宏163
3.44C 宏中的“陷阱”163
单独编译
3.50C++ 中的命名空间39号
3.51使用另一个命名空间中的名称39号
3.52Java 中的包C 40
3.53使用另一个包中的名称C 40
3.54由多部分组成的包名称41号
第四章:语义分析
语义分析器的作用
4.1Java 中的断言182
4.2C 中的断言183
属性文法
4.3自下而上的常量表达式 CFG184
4.4自下而上的 AG 用于常量表达式185
4.5自上而下 AG 计算列表元素数量185
评估属性
4.6解析树的装饰187
4.7自上而下的 CFG 和解析树进行减法188
4.8使用从左到右属性流进行装饰188
4.9自上而下的 AG 减法189
4.10自上而下的常量表达式 AG189
4.11自下而上和自上而下的 AG 构建语法树193
行动惯例
4.12自上而下的操作程序构建语法树198
4.13递归下降和动作例程199
属性的空间管理
4.19自下而上解析的堆栈跟踪,带有操作例程45号
4.20在“埋藏的”记录中寻找继承的属性46号
4.21需要上下文的语法片段47号
4.22上下文的语义钩子47号
4.23破坏 LR CFG 的语义钩子48号
4.24尾部的动作例程49号
4.25用左因子代替语义钩子49号
4.26LL 属性堆栈的操作C 50
4.27语义堆栈的临时管理53号
4.28使用属性堆栈处理列表54号
4.29使用语义堆栈处理列表55号
树文法和语法树装饰
4.14具有类型的计算器语言的自下而上的 CFG201
4.15对整数和实数取平均值的语法树201
4.16具有类型的计算器语言的树语法201
4.17Tree AG 是一种具有类型的计算器语言203
4.18使用示例 4.17 中的 AG 装饰一棵树206
第五章:目标机架构
内存层次结构
5.1内存层次统计61号
数据表示
5.2大端和小端63号
5.3十六进制数65号
5.4二进制补码66号
5.5二进制补码加法溢出66号
5.6有偏指数68号
5.7IEEE 浮点68号
指令集架构 (ISA)
5.8x86 汇编程序中的if语句72号
5.9比较和测试指令73号
5.10有条件的举动73号
架构与实现
5.11x86 ISA碳 80
5.12ARM ISA81号
5.13x86 和 ARM 寄存器集82号
针对现代处理器进行编译
5.14性能≠时钟频率C.88
5.15填充加载延迟槽91号
5.16重命名寄存器以进行调度92号
5.17简单循环的寄存器分配93号
5.18寄存器分配和指令调度95 号
第 6 章:控制流
表达式求值
6.1典型的函数调用225
6.2典型运算符225
6.3剑桥波兰语(前缀)表示法225
6.4机器学习中的并置225
6.5Smalltalk 中的混合符号226
6.6条件表达式226
6.7复杂的 Fortran 表达式226
6.8四种有影响力的语言的优先地位227
6.9Pascal 优先级中的“陷阱”227
6.10结合性的通用规则227
6.11Haskell 中的用户定义优先级和结合性228
6.12L 值和 r 值230
6.13C 中的 L 值230
6.14C++ 中的 L 值231
6.15变量作为值和引用231
6.16包装器类232
6.17Java 5 和 C# 中的装箱232
6.18Algol 68 中的表达方向233
6.19C 条件中的“陷阱”234
6.20更新作业234
6.21副作用和更新235
6.22赋值运算符235
6.23前缀和后缀增加/减少235
6.24后缀 inc/dec 的优点236
6.25简单多路分配236
6.26多路分配的优点236
6.27明确分配禁止的程序239
6.28不确定的顺序240
6.29取决于顺序的值241
6.30依赖于排序的优化241
6.31优化和数学“定律”242
6.32溢出和算术“恒等式”243
6.33重新排序和数值稳定性243
6.34短路表达式243
6.35通过短路节省时间243
6.36短路指针追逐244
6.37短路和其他错误244
6.38可选短路245
结构化和非结构化流程
6.39在 Fortran 中使用 goto 进行控制流246
6.40退出嵌套子程序247
6.41结构化非本地转移248
6.42使用状态代码进行错误检查249
6.43一个简单的 Ruby 延续250
6.44延续重用和无限范围251
测序
6.45随机数生成器的副作用252
选择
6.46Algol 60 中的选择253
6.47elsif/elif253
6.48Lisp 中的cond253
6.49布尔条件的代码生成254
6.50短路代码生成255
6.51布尔值的短路创建255
6.52case语句和嵌套if256
6.53嵌套if的翻译257
6.54跳转表257
6.55C 语言 switch 语句中的 fall-through260
迭代
6.56Fortran 90循环262
6.57Modula-2 for循环262
6.58for循环的明显翻译262
6.59循环翻译,底部有测试263
6.60具有迭代计数的for循环翻译263
6.61幼稚循环翻译中的“陷阱”263
6.62在for循环中更改索引265
6.63检查for循环后的索引265
6.64C 中的组合(for )循环267
6.65带有本地索引的C for循环268
6.66Python 中的简单迭代器268
6.67用于树枚举的 Python 迭代器269
6.68用于树枚举的 Java 迭代器270
6.69C++11 中的迭代270
6.70将“循环体”传递给 Scheme 中的迭代器272
6.71Smalltalk 中的块迭代272
6.72在 Ruby 中使用 procs 进行迭代273
6.73在 C 中模仿迭代器274
6.89Icon 中的简单生成器107号
6.90表达式内的生成器107号
6.91寻求成功的生成C 108
6.92使用多个生成器进行回溯C 108
6.74Algol-W 中的while循环275
6.75Pascal 和 Modula 中的后测试循环275
6.76C 语言中的后测试循环275
6.77C 中的break语句276
6.78在 Ada 中退出嵌套循环276
6.79在 Perl 中退出嵌套循环276
递归
6.80一个“自然迭代”的问题278
6.81一个“自然递归”的问题278
6.82用另一种方式解决问题278
6.83尾递归的迭代实现279
6.84手动创建尾递归代码279
6.85朴素递归斐波那契函数280
6.86线性迭代斐波那契函数281
6.87高效的尾递归斐波那契函数281
6.88无限数据结构的惰性求值283
6.93避免非确定性不对称110 号
6.94使用保护命令进行选择110 号
6.95使用受保护的命令进行循环111号
6.96不确定的消息接收112号
6.97SR 中的非确定性服务器112号
6.98非确定性的简单(不公平)实现113号
6.99非确定性循环实现中的“陷阱”113号
第 7 章:类型系统
7.1利用类型信息的操作297
7.2通过类型信息捕获的错误297
7.3类型作为“可能别名”信息的来源298
概述
7.4void(简单)类型303
7.5不使用void进行操作303
7.6OCaml 中的选项类型303
7.7Swift 中的选项类型304
7.8Ada 中的聚合304
7.9Pascal 中的枚举307
7.10枚举作为常量308
7.11枚举类型与枚举类型的相互转换308
7.12枚举的杰出值308
7.13在 Java 中模拟不同的枚举值309
7.14Pascal 中的子范围309
7.15Ada 中的子范围310
7.16子范围类型的空间要求310
类型检查
7.17类型的细微差异313
7.18类型的其他细微差异313
7.19结构等价问题314
7.20别名类型314
7.21语义等效的别名类型315
7.22语义上不同的别名类型315
7.23Ada 中的派生类型和子类型315
7.24名称与结构等价性316
7.25需要给定类型的上下文316
7.26Ada 中的类型转换317
7.27C 中的类型转换318
7.28Ada 中未经检查的转换319
7.29C++ 中的转换和非转换强制类型转换319
7.30C 中的强制转换320
7.31强制转换与加数重载322
7.32Java 对象容器323
7.33子范围类型的推断324
7.34集合的类型推断325
7.35C# 中的var声明325
7.36避免混乱的声明325
7.37C++11 中的decltype326
7.38OCaml 中的斐波那契函数327
7.39使用显式类型检查327
7.40表达式类型328
7.41类型不一致328
7.42多态函数329
7.43统一的一个简单例子330
参数多态性
7.44在 OCaml 或 Haskell 中查找最小值331
7.45Scheme 中的隐式多态性332
7.46Ruby 中的鸭子类型332
7.47Ada 中的通用min函数333
7.48C++ 中的通用队列333
7.49通用参数333
7.50使用Ada 中的约束336
7.51Java 中的通用排序例程336
7.52C# 中的通用排序例程337
7.53C++ 中的通用排序例程337
7.54C++ 中的泛型类实例338
7.55Ada 中的通用子程序实例338
7.56C++ 中的隐式实例338
7.58C++ 中的通用仲裁器类119号
7.59模板函数体移至 .c 文件121号
7.60C++11 中的外部模板122号
7.61C++ 模板中的实例化时错误122号
7.62Java 中的通用Arbiter类124号
7.63Java 泛型参数的通配符和界限C 125
7.64类型擦除和隐式强制类型转换126号
7.65Java 中的未经检查的警告127号
7.66Java 泛型和内置类型127号
7.67在 C# 中共享泛型实现128号
7.68C# 泛型和内置类型128号
7.69C# 中的通用Arbiter类128号
7.70Arbiter接口中的逆变128号
7.71协方差130 号
7.72选择者作为代表130 号
相等性测试和分配
7.57Scheme 中的相等性测试340
第 8 章:复合类型
记录(结构)
8.1交流结构352
8.2访问记录字段352
8.3嵌套记录352
8.4OCaml 记录和元组353
8.5记录类型的内存布局353
8.6嵌套记录作为值354
8.7嵌套记录作为引用354
8.8打包类型的布局354
8.9记录的分配和比较355
8.10通过对字段进行排序来最小化漏洞356
8.11C 语言中的 union357
8.12变体记录的动机358
8.59传统 C 中的嵌套结构和联合136号
8.60Pascal 中的变体记录137号
8.61C11 和 C++11 中的匿名联合137号
8.62使用 union 来破坏类型安全138号
8.63OCaml 中的类型安全联合139号
8.64Ada 变体和标签(判别式)C 140
8.65Ada 中的可区分子类型141号
8.66Ada 中的判别数组141号
8.67派生类型作为联合的替代142号
数组
8.13数组声明359
8.14多维数组360
8.15多维数组与组合数组360
8.16C 中的数组的数组361
8.17数组切片操作362
8.18C 语言中动态形状的局部数组365
8.19详细数组的堆栈分配365
8.20Fortran 90 中的精细数组366
8.21Java 和 C# 中的动态字符串367
8.22行主序与列主序数组布局368
8.23阵列布局和缓存性能368
8.24连续与行指针数组布局370
8.25索引连续数组371
8.26数组索引的静态部分和动态部分372
8.27索引复杂结构373
8.28索引行指针数组374
字符串
8.29C 和 C++ 中的字符转义375
8.30C 语言中的char*赋值376
8.31Pascal 中的集合类型376
8.32在 Go 中使用 map 模拟集合377
指针和递归类型
8.33OCaml 中的树类型379
8.34Lisp 中的树类型379
8.35OCaml 中的相互递归类型380
8.36Ada 和 C 中的树类型382
8.37分配堆节点382
8.38面向对象堆节点分配382
8.39基于指针的树382
8.40指针取消引用382
8.41Ada 中的隐式解除引用383
8.42OCaml 中的指针取消引用383
8.43Lisp 中的赋值384
8.44C 中的数组名称和指针384
8.45C 语言中的指针比较和减法386
8.46C 中的指针和数组声明386
8.47C 语言中的数组作为参数387
8.48C 语言中的sizeof387
8.49显式存储回收388
8.50在 C++ 中对堆栈变量的悬垂引用388
8.51在 C++ 中对堆变量的悬垂引用388
8.68使用墓碑检测悬空引用144号
8.69使用锁和钥匙进行悬垂参考检测146号
8.52引用计数和循环结构391
8.53带指针反转的堆跟踪394
列表
8.54ML 和 Lisp 中的列表398
8.55列表符号399
8.56Lisp 中的基本列表操作400
8.57OCaml 中的基本列表操作400
8.58列表推导400
8.70文件作为内置类型C 150
8.71开放操作C 150
8.72关闭操作C 150
8.73Fortran 中的格式化输出152号
8.74标签格式152号
8.75打印到标准输出153号
8.76Ada 中的格式化输出153号
8.77重载put例程154号
8.78C 中的格式化输出154号
8.79格式字符串中的文本155号
8.80C 中的格式化输入155号
8.81C++ 中的格式化输出156号
8.82流操纵器157号
8.83C++ 中的数组输出157号
8.84更改默认格式158号
第 9 章:子程序和控制抽象
回顾堆栈布局
9.1运行时堆栈的布局(重复)412
9.2相对于帧指针的偏移量412
9.3静态和动态链接412
9.4嵌套例程的可见性413
调用序列
9.5典型的调用序列415
9.56使用显示器进行非本地访问163号
9.57LLVM/ARM 堆栈布局167号
9.58LLVM/ARM 调用序列C 170
9.59gcc /x86-32 堆栈布局172号
9.60gcc /x86-32 调用序列172号
9.61子程序闭包蹦床174号
9.62x86-64 红区175号
9.63在 SPARC 上注册窗口177号
9.6请求内联子程序419
9.7内联和递归420
参数传递
9.8中缀运算符422
9.9Lisp 和 Smalltalk 中的控制抽象422
9.10将参数传递给子程序423
9.11值及参考参数423
9.12按值/结果调用424
9.13在 C 中模拟引用调用424
9.14C 中的const参数426
9.15C++ 中的引用参数428
9.16C++ 中的引用作为别名428
9.17使用内联别名简化代码428
9.18从函数返回引用429
9.19C++11 中的 R 值引用430
9.20Ada 中的子程序作为参数431
9.21Scheme 中的一等子程序431
9.22OCaml 中的一等子程序432
9.23C 和 C++ 中的子程序指针432
9.64Jensen 的设备C 180
9.24Ada 中的默认参数433
9.25Ada 中的命名参数435
9.26使用命名参数进行自我文档化436
9.27C 语言中可变数量的参数436
9.28Java 中可变数量的参数437
9.29C# 中可变数量的参数438
9.30return语句438
9.31返回值的增量计算438
9.32Go 中明确命名的返回值439
9.33多值返回439
异常处理
9.34PL/I 中的ON条件441
9.35C++ 中的简单try块441
9.36嵌套try442
9.37将异常从被调用例程中传播出去442
9.38什么例外?444
9.39参数化异常444
9.40C++ 中的多个处理程序445
9.41OCaml 中的异常处理程序446
9.42Python 中的finally子句447
9.43堆叠异常处理程序447
9.44每个处理程序有多个异常447
9.45C 中的setjmplongjmp449
协程
9.46显式交错并发计算451
9.47交错协程451
9.48仙人掌堆453
9.49切换协程455
9.65基于协程的迭代器调用183号
9.66基于协程的迭代器实现183号
9.67C# 中的迭代器使用184号
9.68C# 迭代器的实现185号
9.69复杂物理系统的顺序模拟187号
9.70基于协程的交通模拟的初始化187号
9.71在交通模拟中穿越街道段188号
9.72安排协程以供将来执行188号
9.73红绿灯处车辆排队188号
9.74等红灯189号
9.75为未来的执行而睡觉189号
活动
9.50信号蹦床457
9.51C# 中的事件处理程序459
9.52匿名委托处理程序459
9.53Java 中的事件处理程序460
9.54匿名内部类处理程序460
9.55使用 lambda 表达式处理事件460
第 10 章:数据抽象和面向对象
面向对象编程
10.1C++ 中的list_node类473
10.2使用list_node的列表类473
10.3内联(扩展)对象的声明475
10.4构造函数参数475
10.5方法声明无定义476
10.6单独的方法定义477
10.7C# 中的属性索引器方法477
10.8列表派生的队列类478
10.9隐藏基类的成员479
10.10在派生类中重新定义方法479
10.11访问基类成员480
10.12在 Eiffel 中重命名方法480
10.13包含列表的队列480
10.14通用列表的基类481
10.15类型特定扩展的问题482
10.16如何命名未知类型?483
10.17C++ 中的泛型列表483
封装和继承
10.18Ada 中的数据隐藏486
10.19隐藏this参数487
10.20隐藏继承的方法488
10.21C++ 中的受保护基类488
10.22Java 中的内部类490
10.23Ada 2005 中的列表和队列抽象491
10.24C# 中的扩展方法494
初始化和终止
10.25在 Eiffel 中命名构造函数496
10.26Smalltalk 中的元类497
10.27C++ 中的声明和构造函数498
10.28复制构造函数499
10.29临时对象499
10.30返回值优化500
10.31Eiffel 构造函数和扩展对象501
10.32基类构造函数参数的规范502
10.33成员构造函数参数的规范502
10.34构造函数转发503
10.35Java 中基类构造函数的调用503
10.36使用析构函数回收空间504
动态方法绑定
10.37基类上下文中的派生类对象505
10.38静态和动态方法绑定506
10.39动态绑定的必要性507
10.40C++ 和 C# 中的虚方法508
10.41Ada 95 中的类范围类型508
10.42Java 和 C# 中的抽象方法508
10.43C++ 中的抽象方法509
10.44虚表509
10.45虚拟方法调用的实现509
10.46单继承的实现510
10.47C++ 中的强制类型转换511
10.48Eiffel 和 C# 中的反向赋值511
10.49对象闭包中的虚方法513
10.50封装参数514
混合继承
10.51接口的动机516
10.52将接口混合到派生类中516
10.53混合继承的编译时实现517
10.54使用默认方法520
10.55默认方法的实现520
10.56从两个基类派生521
10.57从两个基类派生(重复)194号
10.58(非重复)多重继承194号
10.59具有多重继承的方法调用195 号
10.60此修正196号
10.61在多个基类中发现方法197号
10.62覆盖不明确的方法197号
10.63重复多重继承198号
10.64C++ 中的共享继承199号
10.65Eiffel 中的复制继承199号
10.66使用复制继承C 200
10.67使用共享继承覆盖方法C 201
10.68共享继承的实现C 201
重新审视面向对象编程
10.69Smalltalk 中的消息操作204号
10.70Mixfix 消息204号
10.71选择为ifTrue: ifFalse:消息C 205
10.72迭代消息C 205
10.73块作为闭包C 206
10.74消息的逻辑循环C 206
10.75定义控制抽象C 206
10.76Smalltalk 中的递归207号
第 11 章:函数式语言
历史起源
函数式编程概念
一点计划
11.1读取-求值-打印循环539
11.2括号的意义540
11.3引用540
11.4动态类型540
11.5类型谓词541
11.6符号的自由语法541
11.7lambda表达式541
11.8功能评估542
11.9if表达式542
11.10使用let嵌套作用域542
11.11使用define进行全局绑定543
11.12基本列表操作543
11.13列表搜索功能544
11.14搜索关联列表545
11.15多路条件表达式545
11.16任务545
11.17测序545
11.18迭代546
11.19将数据评估为代码547
11.20在 Scheme 中模拟 DFA548
一些 OCaml
11.21与解释器交互551
11.22函数调用语法551
11.23函数值552
11.24单位类型552
11.25“物理”与“结构”比较553
11.26最外层声明554
11.27嵌套声明555
11.28递归嵌套函数(重复示例 7.38)555
11.29多态列表运算符555
11.30列表符号556
11.31数组表示法556
11.32字符串作为字符数组557
11.33元组表示法557
11.34记录符号557
11.35可变字段558
11.36参考558
11.37枚举类型的变体558
11.38变体作为联合体558
11.39递归变体559
11.40参数模式匹配559
11.41局部声明中的模式匹配560
11.42match 构造560
11.43守卫561
11.44as关键字561
11.45function关键字561
11.46运行时模式匹配562
11.47图案覆盖范围562
11.48根据函数返回的元组进行模式匹配562
11.49没有elseif563
11.50OCaml 中的插入排序563
11.51一个简单的异常564
11.52带参数的异常564
11.53捕获异常564
11.54在 OCaml 中模拟 DFA565
重新审视评估顺序
11.55应用和正常顺序评估567
11.56正常顺序避免不必要的工作568
11.57避免使用惰性求值570
11.58基于流的程序执行571
11.59使用流进行交互式 I/O572
11.60Haskell 中的伪随机数572
11.61IO monad的状态574
11.62动作的功能组合574
11.63流和 I/O monad575
高阶函数
11.64Scheme 中的map函数576
11.65Scheme 中的折叠(缩减)576
11.66OCaml 中的折叠576
11.67组合高阶函数576
11.68柯里化的部分应用577
11.69通用柯里化函数577
11.70元组作为 OCaml 函数参数578
11.71单例参数上的可选括号578
11.72OCaml 中的简单柯里化函数578
11.73柯里化的简写形式579
11.74在 OCaml 中构建fold_left579
11.75OCaml 与 Scheme 中的柯里化580
11.76声明性(非构造性)函数定义580
11.77函数作为映射212号
11.78函数作为集合212号
11.79作为幂集元素的函数213号
11.80功能空间213号
11.81高阶函数作为集合213号
11.82将函数柯里化为集合213号
11.83并置作为功能应用214号
11.84Lambda 演算语法214号
11.85使用 λ 绑定参数214号
11.86自由变量215 号
11.87命名函数以供将来参考215 号
11.88评估规则215 号
11.89减少算术运算的增量215 号
11.90埃塔还原216号
11.91简化为最简形式216号
11.92非终止应用阶减少217号
11.93布尔值和条件218号
11.94递归的 Beta 抽象218号
11.95定点组合器Y218号
11.96Lambda 演算列表运算符219号
11.97列出操作员身份219号
11.98Lambda 表达式的嵌套221号
11.99配对参数和柯里化221号
函数式编程概览
第 12 章:逻辑语言
逻辑编程概念
12.1霍恩条款592
12.2解决592
12.3统一592
序言
12.4原子、变量、范围和类型593
12.5结构和谓词593
12.6事实和规则593
12.7查询594
12.8Prolog 中的解析595
12.9Prolog 和 ML 中的统一595
12.10平等与统一595
12.11无需实例化596
12.12Prolog 中的列表符号596
12.13函数、谓词和双向规则597
12.14算术和 is 谓词597
12.15搜索树探索598
12.16回溯和实例化599
12.17规则评估顺序600
12.18无限回归600
12.19Prolog 中的井字游戏600
12.20切工604
12.21\+ 及其实现605
12.22使用 cut 修剪不需要的答案605
12.23使用剪切进行选择605
12.24循环失败605
12.25使用无界生成器进行循环606
12.26使用get输入字符607
12.27Prolog 程序作为数据607
12.28修改 Prolog 数据库608
12.29井字游戏(完整游戏)608
12.30谓词608
12.31在运行时创建术语610
12.32追求动态目标611
12.33自定义数据库浏览611
12.34谓词作为数学对象612
12.39命题226号
12.40不同的表达方式226号
12.41转换为子句形式227号
12.42转换为 Prolog228号
12.43分离左侧228号
12.44左侧为空229号
12.45定理证明是寻找矛盾229号
12.46斯科勒姆常数230 号
12.47斯科伦函数230 号
12.48斯科勒姆化的局限性230 号
逻辑编程透视
12.35排序速度非常慢613
12.36Prolog中的快速排序614
12.37否定即失败615
12.38否定和实例616
第 13 章:并发
背景和动机
13.1C# 中的独立任务626
13.2简单的竞争条件626
13.3多线程网络浏览器627
13.4调度循环网络浏览器628
13.5缓存一致性问题632
并发编程基础
13.6co-gin 的一般形式638
13.7OpenMP 中的共同开始639
13.8OpenMP 中的并行循环639
13.9C# 中的并行循环639
13.10Fortran 95 中的Forall640
13.11OpenMP 的减少641
13.12Ada 中的精细任务641
13.13共同开始与分叉/加入642
13.14Ada 中的任务类型642
13.15Java 2 中的线程创建643
13.16在 C# 中创建线程644
13.17Java 5 中的线程池645
13.18在 Cilk 中生成同步645
13.19使用 fork/join 建模子程序646
13.20进程上的多路复用线程647
13.21单处理器上的协作多线程648
13.22抢占式多线程中的竞争条件650
13.23上下文切换期间禁用信号651
实现同步
13.24基本 test_and_set 锁654
13.25測試-測試-設置654
13.26有限元分析中的障碍655
13.27“逆向思维”障碍656
13.28Java 7 移相器656
13.29使用CAS进行原子更新657
13.30M&S 队列658
13.31写缓冲区和一致性659
13.32分布式一致性661
13.33使用volatile避免数据竞争662
13.34在进程上调度线程663
13.35线程调度中的竞争条件664
13.36“旋转然后让出”锁665
13.37缓冲区受限问题666
13.38信号量的实现667
13.39带信号量的有界缓冲区668
语言级结构
13.40有界缓冲区监视器670
13.41如何等待信号(提示或绝对)671
13.42原始 CCR 语法674
13.43Java 中的synchronized语句676
13.44在 Java 中以提示形式通知676
13.45Java 5 中的锁定变量677
13.46Java 5 中的多重条件678
13.47一个简单的原子块680
13.48有交易的有限缓冲区680
13.49原子块的翻译681
13.50Multilisp 中的未来构造684
13.51C# 中的 Future684
13.52C++11 中的 Future685
13.53命名进程、端口和条目235 号
13.54Ada 中的入口调用235 号
13.55Go 中的 Channels236号
13.56Go 中的远程调用237号
13.57Java 中的数据报消息238号
13.58Java 中基于连接的消息238号
13.59发送语义的三个主要选项C 240
13.60缓冲依赖型死锁241号
13.61致谢242号
13.62Ada 83 中的有界缓冲区245 号
13.63超时和分布式终止246号
13.64Go 中的有界缓冲区246号
13.65Erlang 中的有界缓冲区247号
13.66在 Erlang 中查看消息247号
13.67RPC 服务器系统251号
第 14 章:脚本语言
什么是脚本语言?
14.1传统语言和脚本语言中的简单程序702
14.2Perl 中的强制转换703
问题领域
14.3“通配符”和“通配符”706
14.4shell 中的For循环706
14.5一行一个循环706
14.6shell 中的条件测试707
14.7管道708
14.8输出重定向708
14.9stderrstdout的重定向708
14.10Heredocs(内联输入)709
14.11文件名中存在空格问题709
14.12单引号和双引号709
14.13子壳层709
14.14shell 中括号引用的块710
14.15基于模式的列表生成710
14.16用户定义的 shell 函数710
14.17脚本文件中的# !约定711
14.18使用sed提取 HTML 标头713
14.19sed中的单行脚本713
14.20使用awk提取 HTML 标头714
14.21awk中的字段715
14.22在awk中将标题大写715
14.23使用 Perl 提取 HTML 标头716
14.24Perl 中的“强制退出”脚本718
14.25Python 中的“强制退出”脚本720
14.26Ruby 中的方法调用语法722
14.27Ruby 中的“强制退出”脚本722
14.28使用 Emacs Lisp 对行进行编号725
编写万维网脚本
14.29使用 CGI 脚本进行远程监控728
14.30带有 CGI 脚本的 Adder Web 表单728
14.31使用 PHP 脚本进行远程监控731
14.32碎片化的 PHP 脚本731
14.33带有 PHP 脚本的 Adder Web 表单732
14.34自发布加法器网络表单732
14.35使用 JavaScript 编写的 Adder Web 表单734
14.36在网页中嵌入小程序735
14.81HTML 中的内容与呈现258号
14.82格式正确的 XHTML259号
14.83使用 XHTML 显示喜爱的引言261号
14.84XHTML 元素的 XPath 名称262号
14.85使用 XSLT 创建参考列表262号
创新功能
14.37Python 中的作用域规则740
14.38R 中的超级分配740
14.39Perl 中的静态和动态作用域741
14.40在 Perl 中访问全局变量742
14.41POSIX RE 中的基本操作744
14.42POSIX RE 中的额外量词744
14.43零长度断言744
14.44字符类744
14.45点 (.) 字符745
14.46字符类中的否定和引用745
14.47预定义 POSIX 字符类745
14.48Perl 中的 RE 匹配745
14.49在 Perl 中否定匹配746
14.50Perl 中的 RE 替换746
14.51RE 匹配中的尾随修饰符746
14.52贪婪和最小匹配748
14.53HTML 标头的最小匹配748
14.54扩展 RE 中的变量插值748
14.55扩展 RE 中的变量捕获749
14.56扩展 RE 中的反向引用750
14.57解析浮点文字750
14.58隐式捕获前缀、匹配和后缀750
14.59Ruby 和 Perl 中的强制转换751
14.60Perl 中的强制转换和上下文751
14.61Ruby 中的显式转换752
14.62Perl 数组753
14.63Perl 哈希753
14.64Python 和 Ruby 中的数组和哈希754
14.65Ruby 中的数组访问方法755
14.66Python 中的元组755
14.67Python 中的集合755
14.68PHP、Tcl 和 JavaScript 中的混合类型755
14.69Python 和其他语言中的多维数组755
14.70Perl 中的标量和列表上下文756
14.71使用 wantarray 确定调用上下文757
14.72Perl 中的一个简单类757
14.73在 Perl 中调用方法758
14.74Perl 中的继承759
14.75通过use base继承759
14.76JavaScript 中的原型760
14.77在 JavaScript 中重写实例方法761
14.78JavaScript 中的继承761
14.79Python 和 Ruby 中的构造函数762
14.80在 Python 和 Ruby 中命名类成员762
第 15 章:构建可运行程序
后端编译器结构
15.1编译阶段776
15.2GCD 程序抽象语法树(重演)776
中级形式
15.3图 15.1中的中间形式781
15.19GIMPLE 中的 GCD 程序273号
15.20RTL insn序列276号
15.4计算海伦公式783
代码生成
15.5更简单的编译器结构784
15.6用于代码生成的属性语法785
15.7基于堆栈的寄存器分配787
15.8GCD 程序目标代码788
地址空间组织
15.9Linux 地址空间布局792
集会
15.10汇编作为最终的编译过程792
15.11直接生成目标代码794
15.12压缩nops794
15.13相对分支和绝对分支794
15.14伪指令795
15.15汇编程序指令795
15.16目标文件中地址的编码796
链接
15.17静态链接798
15.18对标头进行校验以确保一致性799
15.21x86/Linux 下的 PIC280 号
15.22x86 上的 PC 相对寻址282号
15.23x86 上的 Linux 中的动态链接282号
第 16 章:运行时程序管理
16.1CLI 作为运行时系统和虚拟机807
虚拟机
16.2“Hello, world” 的常量813
16.3列表插入操作的字节码818
16.39CLI 和 JVM 中的泛型291号
16.40列表插入操作的 CIL292号
机器代码的后期绑定
16.4内联什么时候是安全的?824
16.5推测优化825
16.6CLR 中的动态编译826
16.7CMU Common Lisp 中的动态编译827
16.8Perl 的编译827
16.9Mac 68K 模拟器829
16.10Transmeta Crusoe 处理器829
16.11静态二进制翻译830
16.12动态二进制翻译830
16.13混合口译和笔译830
16.14透明动态翻译831
16.15翻译和虚拟化831
16.16Dynamo 动态优化器831
16.17ATOM 二进制重写器833
检查/自省
16.18查找引用变量的具体类型837
16.19反射不应该做什么838
16.20Java 类命名约定838
16.21获取特定类的信息839
16.22列出 Java 类的方法839
16.23使用反射调用方法840
16.24Ruby 中的反射功能841
16.25Java 中的用户定义注释842
16.26C# 中的用户定义注释842
16.27javadoc842
16.28组件间通信843
16.29LINQ 的属性843
16.30Java 建模语言844
16.31Java 注释处理器845
16.32设置断点847
16.33硬件断点847
16.34设置观察点847
16.35统计抽样848
16.36调用图分析848
16.37查找低 IPC 的基本块849
16.38Haswell 性能计数器849
第 17 章:代码改进
17.1代码改进阶段299号
17.2消除冗余加载和存储C 301
17.3常量折叠C 301
17.4持续传播C 301
17.5公共子表达式消除C 302
17.6复制传播C 302
17.7强度降低C 302
17.8消除无用指令C 303
17.9指令集的利用C 303
17.10组合子程序C 305
17.11语法树和简单控制流图C 305
17.12局部冗余消除结果C 310
17.13转换为 SSA 表格313 号
17.14全局值编号313 号
17.15可用表达式的数据流方程317 号
17.16可用表达式的固定点317 号
17.17全局公共子表达式消除的结果318 号
17.18边分裂变换319 号
17.19活动变量的数据流方程321 号
17.20活动变量的固定点321 号
17.21用于达到定义的数据流方程324 号
17.22提升循环不变量的结果325 号
17.23感应可变强度降低325 号
17.24归纳变量消除326 号
17.25诱导变量优化结果326 号
17.26剩余管道延迟329号
17.27价值依赖 DAG329号
17.28指令调度结果331 号
17.29循环展开的结果332 号
17.30软件流水线的结果333
17.31环路立交337 号
17.32循环平铺(阻塞)337 号
17.33循环分布339 号
17.34循环融合339 号
17.35获得完美的循环嵌套339 号
17.36循环依赖340 号
17.37循环反转和交换341 号
17.38循环倾斜341 号
17.39粗粒度并行化343 号
17.40露天开采343 号
17.41虚拟寄存器的有效范围344 号
17.42登记着色344 号
17.43优化组合子程序346 号

t0010_at0010_bt0010_ct0010_dt0010_et0010_ft0010_gt0010_小时t0010_it0010_jt0010_kt0010_lt0010_mt0010_nt0010_ot0010_pt0010_qt0010_rt0010_st0010_t

参考书目

Bibliography

[亚洲电话+ 96]Amza Cristiana、Cox Alan L.、Dwarkadas Sandhya、Keleher Pete、Lu Honghui、Rajamony Ramakrishnan、Yu Weimin、Zwaenepoel Willy。TreadMarks:工作站网络上的共享内存计算。IEEE计算机。1996;29(2):2 月 18-28 日。

[ACD+96] Amza Cristiana, Cox Alan L., Dwarkadas Sandhya, Keleher Pete, Lu Honghui, Rajamony Ramakrishnan, Yu Weimin, Zwaenepoel Willy. TreadMarks: Shared memory computing on networks of workstations. IEEE Computer. 1996;29(2):18–28 February.

[Ado90]Adobe Systems, Inc. PostScript 语言参考手册。第二版,马萨诸塞州雷丁:Addison-Wesley;1990 年。

[Ado90] Adobe Systems, Inc. PostScript Language Reference Manual. second edition Reading, MA: Addison-Wesley; 1990.

[AF84]Apt Krzysztof R.,Francez Nissim。CSP 的分布式终止约定建模。ACM编程语言和系统事务。1984;6(3):7 月 370-379。

[AF84] Apt Krzysztof R., Francez Nissim. Modeling the distributed termination convention of CSP. ACM Transactions on Programming Languages and Systems. 1984;6(3):370–379 July.

[AFG + 05]Arnold Matthew、Fink Stephen J.、Grove David、Hind Michael、Sweeney Peter F. 虚拟机自适应优化调查。IEEE论文集。2005;93(2):2 月 449-466 日。

[AFG+05] Arnold Matthew, Fink Stephen J., Grove David, Hind Michael, Sweeney Peter F. A survey ofadaptive optimization in virtual machines. Proceedings of the IEEE. 2005;93(2):449–466 February.

[AG96]Adve Sarita V.,Gharachorloo Kourosh。共享内存一致性模型:教程。IEEE计算机。1996;29(12):66-76 十二月。

[AG96] Adve Sarita V., Gharachorloo Kourosh. Shared memory consistency models: A tutorial. IEEE Computer. 1996;29(12):66–76 December.

[AG05]Abrahams David、Gurtovoy Aleksey。C ++ 模板元编程:来自 Boost 及其他语言的概念、工具和技术。马萨诸塞州波士顿:Addison-Wesley;2005 年。

[AG05] Abrahams David, Gurtovoy Aleksey. C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond. Boston, MA: Addison-Wesley; 2005.

[AG06]Arnold Ken、Gosling James。《Java 编程语言》。第四版 Addison-Wesley Professional;2006 年。

[AG06] Arnold Ken, Gosling James. The Java Programming Language. fourth edition Addison-Wesley Professional; 2006.

[AH95]Agesen Ole,Holzle Urs。类型反馈与具体类型推断:面向对象语言优化技术的比较。收录于:第十届 ACM SIGPLAN 面向对象编程系统、语言和应用程序会议论文集;1995:91–107 德克萨斯州奥斯汀,十月。

[AH95] Agesen Ole, Holzle Urs. Type feedback v. concrete type inference: A comparison of optimization techniques for object-oriented languages. In: Proceedings of the Tenth ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages, and Applications; 1995:91–107 Austin, TX, October.

[AK02]Allen Randy、Kennedy Ken。《针对现代架构优化编译器:基于依赖性的方法》。加利福尼亚州旧金山:Morgan Kaufmann;2002 年。

[AK02] Allen Randy, Kennedy Ken. Optimizing Compilers for Modern Architectures: A Dependence-Based Approach. San Francisco, CA: Morgan Kaufmann; 2002.

[AKW88]Aho Alfred V.、Kernighan Brian W. 和 Weinberger Peter J. 《AWK 编程语言》。马萨诸塞州雷丁:Addison-Wesley;1988 年。

[AKW88] Aho Alfred V., Kernighan Brian W., Weinberger Peter J. The AWK Programming Language. Reading, MA: Addison-Wesley; 1988.

[全部69]Allen Frances E. 程序优化。《自动编程年度评论》。1969;5:239-307。

[All69] Allen Frances E. Program optimization. Annual Review in Automatic Programming. 1969;5:239–307.

[ALSU07]Aho Alfred V.、Lam Monica S.、Sethi Ravi 和 Ullman Jeffrey D.编译器:原理、技术和工具。第二版,波士顿,马萨诸塞州:Addison-Wesley;2007 年。

[ALSU07] Aho Alfred V., Lam Monica S., Sethi Ravi, Ullman Jeffrey D. Compilers: Principles, Techniques, and Tools. second edition Boston, MA: Addison-Wesley; 2007.

[Ame78]美国国家标准协会,纽约,纽约州。编程语言最小 BASIC。1978 ANSI X3.60–1978。

[Ame78] American National Standards Institute, New York, NY. Programming Language Minimal BASIC. 1978 ANSI X3.60–1978.

[Ame83]美国国家标准协会,纽约,纽约州。Ada编程语言参考手册。1983年 1 月 ANSI/MIL 1815 A–1983。

[Ame83] American National Standards Institute, New York, NY. Reference Manual for the Ada Programming Language. 1983 January ANSI/MIL 1815 A–1983.

[Ame90]美国国家标准协会,纽约州纽约市。编程语言 C。1990 ANSI/ISO 9899–1990(ANSI X3.159–1989 的修订和重新指定)。

[Ame90] American National Standards Institute, New York, NY. Programming Language C. 1990 ANSI/ISO 9899–1990 (revision and redesignation of ANSI X3.159–1989).

[Ame96a]美国国家标准协会,纽约,纽约州。信息技术 - 编程语言 REXX。1996 ANSI INCITS 274-1996/AMD1-2000 (R2001)。

[Ame96a] American National Standards Institute, New York, NY. Information Technology—Programming Language REXX. 1996 ANSI INCITS 274-1996/AMD1-2000 (R2001).

[Ame96b]美国国家标准协会,纽约,纽约州。编程语言 - Common Lisp。1996年。ANSI X3.226:1994。可从lispworks.com/documentation/common-lisp.html获取。

[Ame96b] American National Standards Institute, New York, NY. Programming Language—Common Lisp. 1996. ANSI X3.226:1994. Available at lispworks.com/documentation/common-lisp.html.

[AO93]Andrews Gregory R.,Olsson Ronald A. SR 编程语言:实践中的并发性。加利福尼亚州雷德伍德城:Benjamin/Cummings;1993 年。

[AO93] Andrews Gregory R., Olsson Ronald A. The SR Programming Language: Concurrency in Practice. Redwood City, CA: Benjamin/Cummings; 1993.

[应用91]Appel Andrew W.《Compiling with Continuations》。英国剑桥:剑桥大学出版社;1991 年。

[App91] Appel Andrew W. Compiling with Continuations. Cambridge, England: Cambridge University Press; 1991.

[应用97]Appel Andrew W.现代编译器实现。英国剑桥:剑桥大学出版社;1997 文本提供 ML、Java 和 C 版本。C 版本由 Maia Ginsburg 专门编写;Java 版本(第二版,2002 年)由 Jens Palsberg 专门编写。

[App97] Appel Andrew W. Modern Compiler Implementation. Cambridge, England: Cambridge University Press; 1997 Text available in ML, Java, and C versions. C version specialized by Maia Ginsburg; Java version (second edition, 2002) specialized by Jens Palsberg.

[Arm07]阿姆斯特朗·乔。《Erlang 到底为什么这么受关注?》《实用程序员》。2007年。可访问pragprog.com/articles/erlang.html

[Arm07] Armstrong Joe. What's all this fuss about Erlang? The Pragmatic Programmers. 2007. Available at pragprog.com/articles/erlang.html.

[手臂13]阿姆斯特朗·乔。《编程 Erlang:面向并发世界的软件》。第二版《实用书架》;2013 年。

[Arm13] Armstrong Joe. Programming Erlang: Software for a Concuirrent World. second edition Pragmatic Bookshelf; 2013.

[第 83 条]Andrews Gregory R.,Schneider Fred B. 并发编程的概念和符号。ACM计算调查。1983;15(1):3 月 3-43 日。

[AS83] Andrews Gregory R., Schneider Fred B. Concepts and notations for concurrent programming. ACM Computing Surveys. 1983;15(1):3–43 March.

[AS96]Abelson Harold,Sussman Gerald Jay。《计算机程序的结构和解释》。第二版,马萨诸塞州剑桥:麻省理工学院出版社;1996 年。与 Julie Sussman 合著。全文和补充资源可在mitpress.mit.edu/sicp/上找到。

[AS96] Abelson Harold, Sussman Gerald Jay. Structure and Interpretation of Computer Programs. second edition Cambridge, MA: MIT Press; 1996. With Julie Sussman. Full text and supplementary resources available at mitpress.mit.edu/sicp/.

[Ass93]计算机协会,纽约,纽约州。在:第二届 ACM SIGPLAN 编程语言历史 (HOPL) 会议论文集,马萨诸塞州剑桥;1993 年 4 月在 ACM SIGPLAN 通知,28(3),1993 年 3 月。

[Ass93] Association for Computing Machinery, New York, NY. In: Proceedings of the Second ACM SIGPLAN History of Programming Languages (HOPL) Conference, Cambridge, MA; 1993 April In ACM SIGPLAN Notices, 28(3), March 1993.

[Ass07]计算机协会,纽约,纽约州。第三届 ACM SIGPLAN 编程语言历史 (HOPL) 会议论文集,加利福尼亚州圣地亚哥;2007 年 6 月。

[Ass07] Association for Computing Machinery, New York, NY. In: Proceedings of the Third ACM SIGPLAN History of Programming Languages (HOPL) Conference, San Diego, CA; 2007 June.

[攻击73]Stella Atkins M. 使用受限编译器在 Algol 60 中实现相互递归。《ACM 通讯》。1973;16(1):47-48。

[Atk73] Stella Atkins M. Mutual recursion in Algol 60 using restricted compilers. Communications of the ACM. 1973;16(1):47–48.

[AU72]Aho Alfred V.、Ullman Jeffrey D. 《解析、翻译和编译理论》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1972 年两卷本。

[AU72] Aho Alfred V., Ullman Jeffrey D. The Theory of Parsing, Translation and Compiling. Englewood Cliffs, NJ: Prentice-Hall; 1972 Two-volume set.

[AWZ88]Alpern Bowen、Wegman Mark N.、Kenneth Zadeck F.检测程序中变量的相等性。见:第十五届 ACM 编程语言原理研讨会会议记录;1988 年 1 月:1-11 加利福尼亚州圣地亚哥。

[AWZ88] Alpern Bowen, Wegman Mark N., Kenneth Zadeck F. Detecting equality of variables in programs. In: Conference Record of the Fifteenth ACM Symposium on Principles of Programming Languages; 1988:1–11 San Diego, CA, January.

[Ayc03]Aycock John。即时生产简史。ACM计算调查。2003;35(2):6 月 97-113。

[Ayc03] Aycock John. A brief history of just-in-time. ACM Computing Surveys. 2003;35(2):97–113 June.

[BA08]Boehm Hans-J.、Adve Sarita V. C++ 并发内存模型基础。收录于:SIGPLAN 2008 编程语言设计和实现会议论文集;2008:68–78 亚利桑那州图森,六月。

[BA08] Boehm Hans-J., Adve Sarita V. Foundations of the C++ concurrency memory model. In: Proceedings of the SIGPLAN 2008 Conference on Programming Language Design and Implementation; 2008:68–78 Tucson, ZA, June.

[Bac78]Backus John W. 编程能从冯·诺依曼风格中解放出来吗?一种函数式风格及其程序代数。ACM通讯。1978;21(8):613–641 八月 1977 年图灵奖演讲。

[Bac78] Backus John W. Can programming be liberated from the von Neumann style? A functional style and its algebra of programs. Communications of the ACM. 1978;21(8):613–641 August The 1977 Turing Award lecture.

[糟糕+ 09]Bocchino Robert L. Jr.、Adve Vikram S.、Dig Danny、Adve Sarita、Heumann Stephen、Komuravelli Rakesh、Overbey Jeffrey、Simmons Patrick、Sung Hyojin、Vakilian Mohsen。确定性并行 Java 的类型和效果系统。收录于:第 24 届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集,佛罗里达州奥兰多;2009 年 10 月。

[BAD+09] Bocchino Robert L. Jr., Adve Vikram S., Dig Danny, Adve Sarita, Heumann Stephen, Komuravelli Rakesh, Overbey Jeffrey, Simmons Patrick, Sung Hyojin, Vakilian Mohsen. A type and effect system for deterministic parallel Java. In: Proceedings of the Twenty-Fourth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, Orlando, FL; 2009 October.

[袋子89]Bagrodia Rajive。CSP 中的异步进程同步。ACM编程语言和系统事务。1989;11(4):10 月 585-597 日。

[Bag89] Bagrodia Rajive. Synchronizatiom of asynchronous processes in CSP. ACM Transactions on Programming Languages and Systems. 1989;11(4):585–597 October.

[球90]Bershad Brian N.、Anderson Thomas E.、Lazowska Edward D.、Levy Henry M. 轻量级远程过程调用。ACM计算机系统学报。1990;8(1):2 月 37-55 日。

[BALL90] Bershad Brian N., Anderson Thomas E., Lazowska Edward D., Levy Henry M. Lightweight remote procedure call. ACM Transactions on Computer Systems. 1990;8(1):37–55 February.

[Ban97]Banerjee Utpal。《依赖性分析》,《重构编译器的循环转换》第 3 卷。马萨诸塞州波士顿:Kluwer Academic Publishers;1997 年。

[Ban97] Banerjee Utpal. Dependence Analysis, volume 3 of Loop Transformations for Restructuring Compilers. Boston, MA: Kluwer Academic Publishers; 1997.

[酒吧84]Barendregt Hendrik Pieter。《Lambda 演算:其语法和语义》,《逻辑与数学基础研究》第 103 卷。修订版荷兰:北荷兰,阿姆斯特丹;1984 年。

[Bar84] Barendregt Hendrik Pieter. The Lambda Calculus: Its Syntax and Semantics, volume 103 of Studies in Logic and the Foundations of Mathematics. revised edition The Netherlands: North-Holland, Amsterdam; 1984.

[BCR04]Bacon David F.、Cheng Perry、Rajan VT垃圾收集统一理论。收录于:第十九届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;2004 年 10 月,加拿大不列颠哥伦比亚省温哥华,50–68 页。

[BCR04] Bacon David F., Cheng Perry, Rajan V.T. A unified theory of garbage collection. In: Proceedings of the Nineteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 2004:50–68 Vancouver, BC, Canada, October.

[BDB00]Bala Vasanth、Duesterwald Evelyn、Banerjia Sanjeev。Dynamo :透明的动态优化系统。收录于:SIGPLAN 2000 编程语言设计和实现会议论文集;2000 年 6 月,加拿大不列颠哥伦比亚省温哥华,1-12。

[BDB00] Bala Vasanth, Duesterwald Evelyn, Banerjia Sanjeev. Dynamo: A transparent dynamic optimization system. In: Proceedings of the SIGPLAN 2000 Conference on Programming Language Design and Implementation; 2000:1–12 Vancouver, BC, Canada, June.

[BDMN73]Birtwistle Graham M.、Dahl Ole-Johan、Myhrhaug Bjorn、Nygaard Kristen。模拟开始。宾夕法尼亚州费城:Auerback Publishers, Inc.; 1973年。

[BDMN73] Birtwistle Graham M., Dahl Ole-Johan, Myhrhaug Bjorn, Nygaard Kristen. SIMULA Begin. Philadelphia, PA: Auerback Publishers, Inc.; 1973.

[BEC97]Beck Leland L.系统软件:系统编程简介。第三版,马萨诸塞州雷丁:Addison-Wesley;1997 年。

[Bec97] Beck Leland L. System Software: An Introduction to Systems Programming. third edition Reading, MA: Addison-Wesley; 1997.

[Bee70]Beech David。PL/I 的结构视图。ACM计算调查。1970;2(1):3 月 33-64 日。

[Bee70] Beech David. A structural view of PL/I. ACM Computing Surveys. 1970;2(1):33–64 March.

[贝尔05]贝拉德·法布里斯。QEMU,一种快速便携的动态翻译器。摘自:USENIX 2005 年度技术会议论文集;2005:41-46 加利福尼亚州阿纳海姆,4 月。

[Bel05] Bellard Fabrice. QEMU, a fast and portable dynamic translator. In: Proceedings of the USENIX 2005 Annual Technical Conference; 2005:41–46 Anaheim, CA, April.

[本00]Bentley John L.编程精粹。第一版 Addison-Wesley Professional;2000 1986 年。

[Ben00] Bentley John L. Programming Pearls. First edition Addison-Wesley Professional; 2000 1986.

[Ber85]Bernstein Robert L. 为 case 语句生成良好的代码。软件——实践与经验。1985;15(10):1021–1024 十月 Sampath Kannan 和 Todd A. Proebsting 的更正出现在第 24 卷第 2 期中。

[Ber85] Bernstein Robert L. Producing good code for the case statement. Software—Practice and Experience. 1985;15(10):1021–1024 October A correction, by Sampath Kannan and Todd A. Proebsting, appears in Volume 24, Number 2.

[BFKM86]Brownston Lee、Farrell Robert、Kant Elaine、Martin Nancy。《OPS5 中的专家系统编程:基于规则的编程简介》。马萨诸塞州雷丁:Addison-Wesley;1986 年。

[BFKM86] Brownston Lee, Farrell Robert, Kant Elaine, Martin Nancy. Programming Expert Systems in OPS5: An Introduction to Rule-Based Programming. Reading, MA: Addison-Wesley; 1986.

[BGS94]Bacon David F.,Graham Susan L.,Sharp Oliver J. 高性能计算的编译器转换。ACM计算调查。1994;26(4):345-420 十二月。

[BGS94] Bacon David F., Graham Susan L., Sharp Oliver J. Compiler transformations for high-performance computing. ACM Computing Surveys. 1994;26(4):345–420 December.

[BHJL07] Andrew Black、Norman Hutchinson、Eric Jul 和 Henry Levy。Emerald 编程语言的发展。载于 HOPL III 论文集 [Ass07],第 11-1–11-51 页。

[BHJL07] Andrew Black, Norman Hutchinson, Eric Jul, and Henry Levy. The development of the Emerald programming language. In HOPL III Proceedings [Ass07], pages 11-1–11-51.

[BHPS61]Bar-Hillel Yehoshua、Perles Micha A.、Shamir Eliahu。简单短语结构语法的形式性质。语音、语言科学和通讯研究的时代文献。 1961;14:143-172。

[BHPS61] Bar-Hillel Yehoshua, Perles Micha A., Shamir Eliahu. On formal properties of simple phrase structure grammars. Zeitschrift feur Phonetik, Sprachwissenschaft und Kommunikationsforschung. 1961;14:143–172.

[BI82]Borning Alan H.,Ingalls Daniel HH Smalltalk-80 中的多重继承。引自:AAAI-82:全国人工智能大会;1982:234–237 宾夕法尼亚州匹兹堡,八月。

[BI82] Borning Alan H., Ingalls Daniel H.H. Multiple inheritance in Smalltalk-80. In: AAAI-82: The National Conference on Artificial Intelligence; 1982:234–237 Pittsburgh, PA, August.

[基本法92]Ball Thomas,Larus James R.最佳程序分析和跟踪。收录于:第十九届 ACM 编程语言原理研讨会会议记录;1992:59-70,新墨西哥州阿尔伯克基,1 月。

[BL92] Ball Thomas, Larus James R. Optimally profiling and tracing programs. In: Conference Record of the Nineteenth ACM Symposium on Principles of Programming Languages; 1992:59–70 Albuquerque, NM, January.

[BM77]Boyer Robert S.,Strother Moore J. 一种快速字符串搜索算法。《ACM 通讯》。1977;20(10):762-772 十月。

[BM77] Boyer Robert S., Strother Moore J. A fast string searching algorithm. Communications of the ACM. 1977;20(10):762–772 October.

[BN84]Birrell Andrew D.,Nelson Bruce J. 实现远程过程调用。ACM计算机系统学报。1984;2(1):2 月 39-59 日。

[BN84] Birrell Andrew D., Nelson Bruce J. Implementing remote procedure calls. ACM Transactions on Computer Systems. 1984;2(1):39–59 February.

[博11]Bryant Randal E、David O'Hallaron。计算机系统:程序员的视角。第二版,波士顿,马萨诸塞州:Prentice-Hall;2011 年。

[BO11] Bryant Randal E, O'Hallaron David. Computer Systems: A Programmer's Perspective. second edition Boston, MA: Prentice-Hall; 2011.

[Boe05]Boehm Hans-J。线程不能作为库实现。在:SIGPLAN 2005 编程语言设计和实现会议论文集;2005:261–268 芝加哥,伊利诺斯州,六月。

[Boe05] Boehm Hans-J. Threads cannot be implemented as a library. In: Proceedings of the SIGPLAN 2005 Conference on Programming Language Design and Implementation; 2005:261–268 Chicago, IL, June.

[BOSW98]Bracha Gilad、Odersky Martin、Stoutamire David、Wadler Philip。让未来比过去更安全:为 Java 编程语言添加泛型。收录于:第十三届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1998:183–200 加拿大不列颠哥伦比亚省温哥华,十月。

[BOSW98] Bracha Gilad, Odersky Martin, Stoutamire David, Wadler Philip. Making the future safe for the past: Adding genericity to the Java programming language. In: Proceedings of the Thirteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1998:183–200 Vancouver, BC, Canada, October.

[Bou78]Bourne Stephen R. UNIX shell 简介。Bell System Technical Journal。1978;57(6,第 2 部分):2797–2822 七月至八月。

[Bou78] Bourne Stephen R. An introduction to the UNIX shell. Bell System Technical Journal. 1978;57(6, Part 2):2797–2822 July–August.

[Bri73]Hansen Per Brinch。《操作系统原理》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1973 年。

[Bri73] Hansen Per Brinch. Operating System Principles. Englewood Cliffs, NJ: Prentice-Hall; 1973.

[Bri75]Hansen Per Brinch。编程语言 Concurrent Pascal。IEEE软件工程学报。1975;SE–1(2):6 月 199–207。

[Bri75] Hansen Per Brinch. The programming language Concurrent Pascal. IEEE Transactions on Software Engineering. 1975;SE–1(2):199–207 June.

[Bri81]Hansen Per Brinch。《爱迪生的设计》。软件——实践与经验。1981;11(4):363–396 年 4 月。

[Bri81] Hansen Per Brinch. The design of Edison. Software—Practice and Experience. 1981;11(4):363–396 April.

[Bro87]Brodie Leo。《开始使用 FORTH:面向初学者和专业人士的 FORTH 语言和操作系统简介》。新泽西州恩格尔伍德克利夫斯,第二版:Prentice-Hall 软件系列。Prentice-Hall;1987 年。

[Bro87] Brodie Leo. Starting FORTH: An Introduction to the FORTH Language and Operating System for Beginners and Professionals. Englewood Cliffs, NJ, second edition: Prentice-Hall Software Series. Prentice-Hall; 1987.

[Bro96]Brockschmidt Kraig。OLE 和 COM 如何解决组件软件设计问题。Microsoft Systems Journal。1996;11(5):5 月 63-82 日。

[Bro96] Brockschmidt Kraig. How OLE and COM solve the problems of component software design. Microsoft Systems Journal. 1996;11(5):63–82 May.

[英国标准时间83]Bobrow Daniel G.,Stefik Mark J. LOOPS 手册。Palo Alto,CA:Xerox Palo Alto 研究中心;1983 年技术报告。

[BS83] Bobrow Daniel G., Stefik Mark J. The LOOPS manual. Palo Alto, CA: Xerox Palo Alto Research Center; 1983 Technical report.

[英国标准时间96]Bacon David F.,Sweeney Peter F. C++ 虚拟函数调用的快速静态分析。收录于:第十一届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1996:324–341 加利福尼亚州圣何塞,十月。

[BS96] Bacon David F., Sweeney Peter F. Fast static analysis of C++ virtual function calls. In: Proceedings of the Eleventh ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1996:324–341 San Jose, CA, October.

[BW88]Boehm Hans-Juergen,Weiser Mark。不合作环境下的垃圾收集。软件——实践与经验。1988;18(9):807–820 九月。

[BW88] Boehm Hans-Juergen, Weiser Mark. Garbage collection in an uncooperative environment. Software—Practice and Experience. 1988;18(9):807–820 September.

[CAC + 81]Chaitin Gregory、Auslander Marc、Chandra Ashok、Cocke John、Hopkins Martin、Markstein Peter。通过着色进行寄存器分配。计算机语言。1981;6(1):47-57。

[CAC+81] Chaitin Gregory, Auslander Marc, Chandra Ashok, Cocke John, Hopkins Martin, Markstein Peter. Register allocation via coloring. Computer Languages. 1981;6(1):47–57.

[蔡82]Cailliau R. 如何避免被 Pascal SCHLONKED。ACM SIGPLAN 通知。1982;17(12):12 月 31-40 日。

[Cai82] Cailliau R. How to avoid getting SCHLONKED by Pascal. ACM SIGPLAN Notices. 1982;17(12):31–40 December.

[Can92]坎·戴维。Fortran 退役?争论再起。《ACM 通讯》。1992;35(8):81-89 八月。

[Can92] Cann David. Retire Fortran? A debate rekindled. Communications of the ACM. 1992;35(8):81–89 August.

[CDW04]Chen Hao、Dean Drew、Wagner David。《对一百万行 C 代码进行模型检查》。《网络和分布式系统安全研讨会论文集》;2004 年 2 月,加利福尼亚州圣地亚哥,171-185。

[CDW04] Chen Hao, Dean Drew, Wagner David. Model checking one million lines of C code. In: Proceedings of the Network and Distributed System Security Symposium; 2004:171–185 San Diego, CA, February.

[Cer89]Ceruzzi Paul。《超越极限——飞行进入计算机时代》。马萨诸塞州剑桥:麻省理工学院出版社;1989 年。

[Cer89] Ceruzzi Paul. Beyond the Limits—Flight Enters the ComputerAge. Cambridge, MA: MIT Press; 1989.

[CF58]Curry Haskell B.,Feys Robert。《组合逻辑》,《逻辑与数学基础研究》第 1 卷。荷兰:北荷兰,阿姆斯特丹;1958 年,其中两部分由 William Craig 撰写。

[CF58] Curry Haskell B., Feys Robert. Combinatory Logic, volume 1 of Studies in Logic and the Foundations of Mathematics. The Netherlands: North-Holland, Amsterdam; 1958 With two sections by William Craig.

[CFR + 91]Cytron Ronald、Ferrante Jeanne、Rosen Barry K.、Wegman Mark N.、Kenneth Zadeck F. 高效计算静态单分配形式和控制依赖图。ACM编程语言和系统汇刊。1991;13(4):451-490 十月。

[CFR+91] Cytron Ronald, Ferrante Jeanne, Rosen Barry K., Wegman Mark N., Kenneth Zadeck F. Efficiently computing static single assignment form and the control dependence graph. ACM Transactions on Programming Languages and Systems. 1991;13(4):451–490 October.

[氟钨酸铵12]Tom Christiansen、Foy Brian d、Larry Wall 和 Jon Orwant。《Perl 编程》。第四版 Sebastopol,加州:O'Reilly Media;2012 年。

[CfWO12] Christiansen Tom, foy brian d, Wall Larry, Orwant Jon. Programming Perl. fourth edition Sebastopol, CA: O'Reilly Media; 2012.

[Che92]Andrew Cheese。Parlog的并行执行。德国柏林:Springer-Verlag;1992 年。

[Che92] Cheese Andrew. Parallel Execution of Parlog. Berlin, Germany: Springer-Verlag; 1992.

[Cho56]诺姆·乔姆斯基。描述语言的三种模型。IRE信息理论汇刊。1956年;IT-2(3):9 月 113–124 日。

[Cho56] Chomsky Noam. Three models for the description of language. IRE Transactions on Information Theory. 1956;IT-2(3):113–124 September.

[Cho62]乔姆斯基·诺姆。上下文无关语法和下推存储。见:季度进展报告第 65 号。马萨诸塞州剑桥:麻省理工学院电子研究实验室;1962 年:187-194。

[Cho62] Chomsky Noam. Context-free grammars and pushdown storage. In: Quarterly Progress Report No. 65. Cambridge, MA: MIT Research Laboratory for Electronics; 1962:187–194.

[第 71 章]Courtois Pierre-Jacques、Heymans F.、Parnas David L. “读者”和“写者”的并发控制。ACM通讯。1971;14(10):10 月 667-668 日。

[CHP71] Courtois Pierre-Jacques, Heymans F., Parnas David L. Concurrent control with ‘readers’ and ‘writers’. Communications of the ACM. 1971;14(10):667–668 October.

[楚41]Church Alonzo。《Lambda 转换演算》。新泽西州普林斯顿:普林斯顿大学出版社;1941 年《数学研究年鉴》第 6 期。

[Chu41] Church Alonzo. The Calculi of Lambda-Conversion. Princeton, NJ: Princeton University Press; 1941 Annals of Mathematical Studies #6.

[CL83]Cook Robert P.,LeBlanc Thomas J. 符号表抽象用于实现具有明确范围控制的语言。IEEE软件工程学报。1983;SE–9(1):1 月 8 日到 12 日。

[CL83] Cook Robert P., LeBlanc Thomas J. A symbol table abstraction to implement languages with explicit scope control. IEEE Transactions on Software Engineering. 1983;SE–9(1):8–12 January.

[Cle86]Craig Cleaveland J.数据类型简介。马萨诸塞州雷丁:Addison-Wesley;1986 年。

[Cle86] Craig Cleaveland J. An Introduction to Data Types. Reading, MA: Addison-Wesley; 1986.

[CLFL94]Chase Jeffrey S.、Levy Henry M.、Feeley Michael J.、Lazowska Edward D.。单地址空间操作系统中的共享和保护。ACM计算机系统学报。1994;12(4):271–307 十一月。

[CLFL94] Chase Jeffrey S., Levy Henry M., Feeley Michael J., Lazowska Edward D. . Sharing and protection in a single-address-space operating system. ACM Transactions on Computer Systems. 1994;12(4):271–307 November.

[CM84]Mani Chandy K.,Misra Jayadev。《饮酒哲学家问题》。《ACM 编程语言和系统汇刊》。1984;6(4):632-646 年 10 月。

[CM84] Mani Chandy K., Misra Jayadev. The drinking philosophers problem. ACM Transactions on Programming Languages and Systems. 1984;6(4):632–646 October.

[CM03]Clocksin William F.、Mellish Christopher S. 《Prolog 编程》。第五版德国:Springer-Verlag,柏林;2003 年。

[CM03] Clocksin William F., Mellish Christopher S. Programming in Prolog. fifth edition Germany: Springer-Verlag, Berlin; 2003.

[Coh81]Cohen Jacques。链接数据结构的垃圾收集。ACM计算调查。1981;13(3):9 月 341-367 日。

[Coh81] Cohen Jacques. Garbage collection of linked data structures. ACM Computing Surveys. 1981;13(3):341–367 September.

[Con63]Conway Melvin E. 可分离转换图编译器的设计。ACM通讯。1963;6(7):7 月 396-408 日。

[Con63] Conway Melvin E. Design of a separable transition-diagram compiler. Communications of the ACM. 1963;6(7):396–408 July.

[Cou84]Courcelle Bruno。属性语法:定义、依赖关系分析、证明方法。收录于:Lorho Bernard 主编的《编译器构造方法与工具:高级课程》。英国剑桥:剑桥大学出版社;1984 年:81-102。

[Cou84] Courcelle Bruno. Attribute grammars: Definitions, analysis of dependencies, proof methods. In: Lorho Bernard, ed. Methods and Tools for Compiler Construction: An Advanced Course. Cambridge, England: Cambridge University Press; 1984:81–102.

[CS69]Cocke John,Schwartz Jacob T.编程语言及其编译器:初步说明。纽约,纽约州:纽约大学 Courant 数学科学研究所;1969 年技术报告。

[CS69] Cocke John, Schwartz Jacob T. Programming languages and their compilers: Preliminary notes. New York, NY: Courant Institute of Mathematical Sciences, New York University; 1969 Technical report.

[CS01]Chamberlain Bradford L.,Snyder Lawrence。数组语言支持并行稀疏计算。见:第十五届国际超级计算大会论文集;2001:133–145 意大利索伦托,六月。

[CS01] Chamberlain Bradford L., Snyder Lawrence. Array language support for parallel sparse computation. In: Proceedings of the Fifteenth International Conference on Supercomputing; 2001:133–145 Sorrento, Italy, June.

[CT04]Cooper Keith D.、Torczon Linda。《设计编译器》。加利福尼亚州旧金山:Morgan Kaufmann;2004 年。

[CT04] Cooper Keith D., Torczon Linda. Engineering a Compiler. San Francisco, CA: Morgan Kaufmann; 2004.

[CW85]Cardelli Luca,Wegner Peter。关于理解类型、数据抽象和多态性。ACM计算调查。1985;17(4):471-522 十二月。

[CW85] Cardelli Luca, Wegner Peter. On understanding types, data abstraction, and polymorphism. ACM ComputingSurveys. 1985;17(4):471–522 December.

[CW05]Chonacky Norman,Winch David。Maple、Mathematica 和 Matlab:没有磁带的 3M。科学与工程计算。2005;7(1):8-16。

[CW05] Chonacky Norman, Winch David. Maple, Mathematica, and Matlab: The 3M's without the tape. Computing in Science and Engineering. 2005;7(1):8–16.

[Dar90]Darlington Jared L. 面向目标编程中按目标失败搜索方向。ACM编程语言和系统事务。1990;12(2):224-252 四月。

[Dar90] Darlington Jared L. Search direction by goal failure in goal-oriented programming. ACM Transactions on Programming Languages and Systems. 1990;12(2):224–252 April.

[Dav63]戴维斯·马丁。《从机械证明中消除无关因素》。《应用数学研讨会论文集》,第 15 卷;罗德岛州普罗维登斯:美国数学学会;1963 年:15-30 页。

[Dav63] Davis Martin. Eliminating the irrelevant from mechanical proofs. In: Proceedings of a Symposium in Applied Mathematics, volume 15; Providence, RI: American Mathematical Society; 1963:15–30.

[DB76]Peter Deutsch L.,Bobrow Daniel G. 一种高效的增量式自动垃圾收集器。《ACM 通讯》。1976;19(9):522-526 年 9 月。

[DB76] Peter Deutsch L., Bobrow Daniel G. An efficient incremental automatic garbage collector. Communications of the ACM. 1976;19(9):522–526 September.

[DDH72]Dahl Ole-Johan、Dijkstra Edsger W.、Hoare Charles Antony Richard。结构化编程。纽约:Academic Press;1972 APIC数据处理研究#8。

[DDH72] Dahl Ole-Johan, Dijkstra Edsger W., Hoare Charles Antony Richard. Structured Programming. New York, NY: Academic Press; 1972 A.P.I.C. Studies in Data Processing #8.

[DeR71]DeRemer Franklin L. 简单的 LR(k) 语法。《ACM 通讯》。1971;14(7):7 月 453-460。

[DeR71] DeRemer Franklin L. Simple LR(k) grammars. Communications of the ACM. 1971;14(7):453–460 July.

[DGAFS + 80]Dewar Robert BK、Fisher Jr. Gerald A.、Schonberg Edmond、Froehlich Robert、Bryant Stephen、Goss Clinton F.、Burke Michael。纽约大学 Ada 翻译器和解释器。收录于:ACM SIGPLAN Ada 编程语言研讨会论文集;1980:194–201 马萨诸塞州波士顿,12 月。

[DGAFS+80] Dewar Robert B.K., Fisher Jr. Gerald A., Schonberg Edmond, Froehlich Robert, Bryant Stephen, Goss Clinton F., Burke Michael. The NYU Ada translator and interpreter. In: Proceedings of the ACM SIGPLAN Symposium on the Ada Programming Language; 1980:194–201 Boston, MA, December.

[Dij60]Dijkstra Edsger W. 递归编程。Numerische Mathematik。1960;2:312-318 重印于《编程系统和语言》第 221-228 页,Saul Rosen 编辑。麦格劳-希尔,纽约,1967 年。

[Dij60] Dijkstra Edsger W. Recursive programming. Numerische Mathematik. 1960;2:312–318 Reprinted as pages 221–228 of Programming Systems and Languages, Saul Rosen, editor. McGraw-Hill, New York, NY, 1967.

[Dij65]Dijkstra Edsger W. 并发编程控制中问题的解决。ACM通讯。1965;8(9):569 九月。

[Dij65] Dijkstra Edsger W. Solution ofaproblem in concurrent programming control. Communications of the ACM. 1965;8(9):569 September.

[Dij68a]Dijkstra Edsger W. 协同顺序进程。引自:Genuys F. 编辑。编程语言。英国伦敦:Academic Press;1968:43-112。

[Dij68a] Dijkstra Edsger W. Co-operating sequential processes. In: Genuys F., ed. Programming Languages. London, England: Academic Press; 1968:43–112.

[Dij68b]Dijkstra Edsger W.《转到声明被认为有害》。《ACM 通讯》。1968;11(3):3 月 147-148 日。

[Dij68b] Dijkstra Edsger W. Go To statement considered harmful. Communications of the ACM. 1968;11(3):147–148 March.

[Dij72]Dijkstra Edsger W. 顺序进程的层次化排序。收录于:Hoare Charles Antony Richard、Perrott Ronald H. 编。操作系统技术。英国伦敦:Academic Press;1972:72-93 APIC 数据处理研究 #9 另见Acta Informatica,1(8):115-138,1971 年。

[Dij72] Dijkstra Edsger W. Hierarchical ordering of sequential processes. In: Hoare Charles Antony Richard, Perrott Ronald H., eds. Operating Systems Techniques. London, England: Academic Press; 1972:72–93 A.P.I.C. Studies in Data Processing #9 Also Acta Informatica, 1(8):115–138, 1971.

[Dij75]Dijkstra Edsger W. 受保护的命令、不确定性和程序的形式化推导。《ACM 通讯》。1975;18(8):8 月 453-457 日。

[Dij75] Dijkstra Edsger W. Guarded commands, nondeterminacy, and formal derivation of programs. Communications of the ACM. 1975;18(8):453–457 August.

[Dij76]Dijkstra Edsger W. 《编程原则》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1976 年。

[Dij76] Dijkstra Edsger W. A Discipline of Programming. Englewood Cliffs, NJ: Prentice-Hall; 1976.

[Dij82]Dijkstra Edsger W. 我们如何说出可能伤人的真相?。ACM SIGPLAN 通知。1982;17(5):5 月 13-15 日。

[Dij82] Dijkstra Edsger W. How do we tell truths that might hurt?. ACM SIGPLAN Notices. 1982;17(5):13–15 May.

[Dio78]Dion Bernard A.上下文无关和上下文敏感解析器的局部最小成本错误校正器。威斯康星大学麦迪逊分校;1978 年博士论文《计算机科学技术报告》第 344 号。

[Dio78] Dion Bernard A. Locally Least-Cost Error Correctors for Context-Free and Context-Sensitive Parsers. University of Wisconsin–Madison; 1978 Ph. D. dissertation Computer Sciences Technical Report #344.

[DMM96]Diwan Amer、Eliot J.、Moss B.、McKinley Kathryn S。简单有效地分析静态类型的面向对象程序。收录于:第十一届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1996:292–305 加利福尼亚州圣何塞,十月。

[DMM96] Diwan Amer, Eliot J., Moss B., McKinley Kathryn S. Simple and effective analysis of statically typed object-oriented programs. In: Proceedings of the Eleventh ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1996:292–305 San Jose, CA, October.

[Dol97]Dolby Julian。对象的自动内联分配。摘自:SIGPLAN '97 编程语言设计和实现会议论文集;1997 年:7-17 拉斯维加斯,内华达州,六月。

[Dol97] Dolby Julian. Automatic inline allocation of objects. In: Proceedings of the SIGPLAN '97 Conference on Programming Language Design and Implementation; 1997:7–17 Las Vegas, NV, June.

[Dri93]Driesen Karel。选择器表索引和稀疏数组。收录于:第八届 ACM SIGPLAN 面向对象编程系统、语言和应用程序会议论文集;1993:259–270 华盛顿特区,九月。

[Dri93] Driesen Karel. Selector table indexing and sparse arrays. In: Proceedings of the Eighth ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages, and Applications; 1993:259–270 Washington, DC, September.

[DRSS96]Dawson Steven、Ramakrishnan CR、Skiena Steven、Swift Terrence。统一因式分解的原理和实践。ACM编程语言和系统事务。1996;18(5):9 月 528-563 日。

[DRSS96] Dawson Steven, Ramakrishnan C.R., Skiena Steven, Swift Terrence. Principles and practice of unification factoring. ACM Transactions on Programming Languages and Systems. 1996;18(5):528–563 September.

[DS84]Peter Deutsch L.,Schiffman Allan M. Smalltalk-80 系统的有效实现。见:第 11 届 ACM 编程语言原理研讨会会议记录;1984:297-302 犹他州盐湖城,1 月。

[DS84] Peter Deutsch L., Schiffman Allan M. Efficient implementation of the Smalltalk-80 system. In: Conference Record of the Eleventh ACM Symposium on Principles of Programming Languages; 1984:297–302 Salt Lake City, UT, January.

[DSS06]Dice Dave、Shalev Ori、Shavit Nir。事务锁定 II。摘自:第二十届国际分布式计算研讨会论文集;2006 年 9 月,瑞典斯德哥尔摩,第 194-208 页。

[DSS06] Dice Dave, Shalev Ori, Shavit Nir. Transactional locking II. In: Proceedings of the Twentieth International Symposium on Distributed Computing; 2006:194–208 Stockholm, Sweden, September.

[到期05]Duesterwald Evelyn。动态二进制优化器的设计和工程。IEEE论文集。2005;93(2):2 月 436-448。

[Due05] Duesterwald Evelyn. Design and engineering of a dynamic binary optimizer. Proceedings of the IEEE. 2005;93(2):436–448 February.

[DWA10]DWARF 调试信息格式委员会。DWARF调试信息格式,版本 4。2010年 6 月,可从dwarfstd.org/doc/DWARF4.pdf获取。

[DWA10] DWARF Debugging Information Format Committee. DWARF Debugging Information Format, Version 4. 2010. June Available as dwarfstd.org/doc/DWARF4.pdf.

[Dya95]Dyadkin Lev J. 多框解析器:不再需要手写词法分析器。IEEE软件。1995;12(5):9 月 61-67 日。

[Dya95] Dyadkin Lev J. Multibox parsers: No more handwritten lexical analyzers. IEEE Software. 1995;12(5):61–67 September.

[Eag12]Eager Michael J. DWARF 调试格式简介。The Pragmatic Programmers;2012 年 4 月 可从dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf获取。

[Eag12] Eager Michael J. Introduction to the DWARF debugging format. The Pragmatic Programmers; 2012. April Available as dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf.

[耳朵70]Earley Jay。一种高效的上下文无关解析算法。ACM通讯。1970年 2 月 13(2):94-102。

[Ear70] Earley Jay. An efficient context-free parsing algorithm. Communications of the ACM. 1970;13(2):94–102 February.

[ECM06a]ECMA International,瑞士日内瓦。C # 语言规范。第四版,2006 年 6 月 ECMA-334、ISO/IEC 23270。可用作ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf

[ECM06a] ECMA International, Geneva, Switzerland. C# Language Specification. fourth edition 2006. June ECMA-334, ISO/IEC 23270. Available as ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf.

[ECM06b]ECMA International,瑞士日内瓦。Eiffel :分析、设计和编程语言。第二版,2006 年 6 月 ECMA-367。可在ecma-international.org/publications/standards/Ecma-367.htm上找到。

[ECM06b] ECMA International, Geneva, Switzerland. Eiffel: Analysis, Design and Programming Language. second edition 2006. June ECMA-367. Available at ecma-international.org/publications/standards/Ecma-367.htm.

[ECM11]ECMA International,瑞士日内瓦。ECMAScript语言规范。2011年 5.1 版。6 月 ECMA-262,ISO/IEC 16262:2011。可从ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf获取。

[ECM11] ECMA International, Geneva, Switzerland. ECMAScript Language Specification. 5.1 edition 2011. June ECMA-262, ISO/IEC 16262:2011. Available as ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf.

[ECM13]ECMA International,瑞士日内瓦。JSON数据交换格式。2013年 10 月 ECMA-404。可从ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf获取。

[ECM13] ECMA International, Geneva, Switzerland. The JSON Data Interchange Format. 2013. October ECMA-404. Available as ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf.

[英语84]Engelfriet Joost。属性语法:属性评估方法。收录于:Lorho Bernard 主编的《编译器构造方法与工具:高级课程》。英国剑桥:剑桥大学出版社;1984 年:103-138。

[Eng84] Engelfriet Joost. Attribute grammars: Attribute evaluation methods. In: Lorho Bernard, ed. Methods and Tools for Compiler Construction: An Advanced Course. Cambridge, England: Cambridge University Press; 1984:103–138.

[Enn06]Ennals Robert。软件事务内存不应无锁。英特尔剑桥研究院;2006 年技术报告 IRC-TR-06-052。

[Enn06] Ennals Robert. Software transactional memory should not be lock free. Intel Research Cambridge; 2006 Technical Report IRC-TR-06-052.

[ES90]Ellis Margaret A.、Stroustrup Bjarne。《带注释的 C++ 参考手册》。马萨诸塞州雷丁:Addison-Wesley;1990 年。

[ES90] Ellis Margaret A., Stroustrup Bjarne. The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley; 1990.

[伊芙63]James Evey R.下推式存储机的应用。收录于:1963 年秋季联合计算机会议论文集;新泽西州蒙特维尔:AFIPS 出版社;1963 年 11 月,内华达州拉斯维加斯:215–227。

[Eve63] James Evey R. Application of pushdown store machines. In: Proceedings of the 1963 Fall Joint Computer Conference; Montvale, NJ: AFIPS Press; 1963:215–227 Las Vegas, NV, November.

[整箱10]Fischer Charles N.、Cytron Ron K.、LeBlanc Richard J. Jr. 《制作编译器》。第二版,马萨诸塞州波士顿:Addison-Wesley;2010 年。

[FCL10] Fischer Charles N., Cytron Ron K., LeBlanc Richard J. Jr. Crafting a Compiler. second edition Boston, MA: Addison-Wesley; 2010.

[FCO90]Feo John T.、Cann David、Oldehoeft Rod R。Sisal 语言项目报告。并行和分布式计算杂志。1990;10(4):12 月 349-365。

[FCO90] Feo John T., Cann David, Oldehoeft Rod R. A report on the Sisal language project. Journal of Parallel and Distributed Computing. 1990;10(4):349–365 December.

[FG84]Feuer Alan R.、Gehani Narain 编。比较和评估编程语言:Ada、C、Pascal。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1984 年 Prentice-Hall 软件系列。

[FG84] Feuer Alan R., Gehani Narain, eds. Comparing and Assessing Programming Languages: Ada, C, Pascal. Englewood Cliffs, NJ: Prentice-Hall; 1984 Prentice-Hall Software Series.

[FH95]Fraser Christopher W.,Hanson David R.可重定向 C 编译器:设计和实现。加利福尼亚州雷德伍德城:Benjamin/Cummings;1995 年。

[FH95] Fraser Christopher W., Hanson David R. A Retargetable C Compiler: Design and Implementation. Redwood City, CA: Benjamin/Cummings; 1995.

[FHP92]Fraser Christopher W.、Hanson David R.、Proebsting Todd A. 设计一个简单、高效的代码生成器。ACM编程语言和系统快报。1992;1(3):9 月 213-226 日。

[FHP92] Fraser Christopher W., Hanson David R., Proebsting Todd A. Engineering a simple, efficient code generator generator. ACM Letters on Programming Languages and Systems. 1992;1(3):213–226 September.

[Fie00]Fielding Roy Thomas。《架构风格和基于网络的软件架构设计》。尔湾:加利福尼亚大学;2000 年博士论文。

[Fie00] Fielding Roy Thomas. Architectural Styles and the Design of Network-based Software Architectures. Irvine: University of California; 2000 Ph. D. dissertation.

[金融96]Finkel Raphael A.高级编程语言设计。加州门洛帕克:Addison-Wesley;1996 年。

[Fin96] Finkel Raphael A. Advanced Programming Language Design. Menlo Park, CA: Addison-Wesley; 1996.

[FL80]Fischer Charles N.,Jr Richard J. LeBlanc。Pascal 中的运行时诊断实现。IEEE软件工程学报。1980;SE–6(4):7 月 313–319。

[FL80] Fischer Charles N., Jr Richard J. LeBlanc. Implementation of runtime diagnostics in Pascal. IEEE Transactions on Software Engineering. 1980;SE–6(4):313–319 July.

[Fle76]Fleck Arthur C. 论通过按名称参数传输技术进行内容交换的不可能性。ACM SIGPLAN 通知。1976;11(11):11 月 38-41 日。

[Fle76] Fleck Arthur C. On the impossibility of content exchange through the by-name parameter transmission technique. ACM SIGPLAN Notices. 1976;11(11):38–41 November.

[FMQ80]Fischer Charles N.、Milton Donn R.、Quiring Sam B. 仅使用插入即可实现高效的 LL(1) 错误更正和恢复。Acta Informatica。1980;13(2):2 月 141-154。

[FMQ80] Fischer Charles N., Milton Donn R., Quiring Sam B. Efficient LL(1) error correction and recovery using only insertions. Acta Informatica. 1980;13(2):141–154 February.

[Fra80]Francez Nissim。分布式终止。ACM编程语言和系统事务。1980;2(1):1 月 42-55 日。

[Fra80] Francez Nissim. Distributed termination. ACM Transactions on Programming Languages and Systems. 1980;2(1):42–55 January.

[FRF08]Felber Pascal、Riegel Torvald、Fetzer Christof。基于字的软件事务内存的动态性能调优。收录于:第十三届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2008 年:237–246 犹他州盐湖城,二月。

[FRF08] Felber Pascal, Riegel Torvald, Fetzer Christof. Dynamic performance tuning of word-based software transactional memory. In: Proceedings of the Thirteenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2008:237–246 Salt Lake City, UT, February.

[FSS83]Freudenberger Stefan M.、Schwartz Jacob T.、Sharir Micha。《SETL 优化器使用经验》。《ACM 编程语言和系统汇刊》。1983;5(1):1 月 26-45 日。

[FSS83] Freudenberger Stefan M., Schwartz Jacob T., Sharir Micha. Experience with the SETL optimizer. ACM Transactions on Programming Languages and Systems. 1983;5(1):26–45 January.

[FWH01]Friedman Daniel P.、Wand Mitchell、Haynes Christopher T.《编程语言基本原理》。第二版,马萨诸塞州剑桥:麻省理工学院出版社;2001 年。

[FWH01] Friedman Daniel P., Wand Mitchell, Haynes Christopher T. Essentials of Programming Languages. second edition Cambridge, MA: MIT Press; 2001.

[69 财年]Fenichel Robert R.,Yochelson Jerome C. 用于虚拟内存计算机系统的 Lisp 垃圾收集器。ACM通讯。1969;12(11):611-612 十一月。

[FY69] Fenichel Robert R., Yochelson Jerome C. A Lisp garbage collector for virtual memory computer systems. Communications of the ACM. 1969;12(11):611–612 November.

[英镑+ 94]Geist Al、B​​eguelin Adam、Dongarra Jack、Jiang Weicheng、Manchek Robert、Sunderam Vaidyalingam S. PVM:并行虚拟机:网络并行计算用户指南和教程。马萨诸塞州剑桥:麻省理工学院出版社;1994 年。可在netlib.org/pvm3/book/pvm-book.html上获取。

[GBD+94] Geist Al, Beguelin Adam, Dongarra Jack, Jiang Weicheng, Manchek Robert, Sunderam Vaidyalingam S. PVM: Parallel Virtual Machine: A Users' Guide and Tutorial for Networked Parallel Computing. Cambridge, MA: MIT Press; 1994. Available at netlib.org/pvm3/book/pvm-book.html.

[GBJ + 12]Grune Dick、Bal Henri E.、Jacobs Ceriel JH、Langendoen Koen G.、Reeuwijk Kees van。现代编译器设计。第二版纽约州纽约:施普林格出版社; 2012年。

[GBJ+12] Grune Dick, Bal Henri E., Jacobs Ceriel J.H., Langendoen Koen G., Reeuwijk Kees van. Modern Compiler Design. second edition New York, NY: Springer-Verlag; 2012.

[GDDC97]Grove David、DeFouw Greg、Dean Jeffrey、Chambers Craig。面向对象语言中的调用图构造。收录于:第十二届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1997:108–124 亚特兰大,佐治亚州,十月。

[GDDC97] Grove David, DeFouw Greg, Dean Jeffrey, Chambers Craig. Call graph construction in object-oriented languages. In: In Proceedings of the Twelfth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1997:108–124 Atlanta, GA, October.

[GFH82]Ganapathi Mahadevan、Fischer Charles N.、Hennessy John L. 可重定向编译器代码生成。ACM计算调查。1982;14(4):573-592 十二月。

[GFH82] Ganapathi Mahadevan, Fischer Charles N., Hennessy John L. Retargetable compiler code generation. ACM Computing Surveys. 1982;14(4):573–592 December.

[GG78]Steven Glanville R.,Graham Susan L.一种编译器代码生成的新方法。摘自:第五届 ACM 编程语言原理研讨会会议记录;1978:231–240 亚利桑那州图森,1 月。

[GG78] Steven Glanville R., Graham Susan L. A new method for compiler code generation. In: Conference Record of the Fifth Annual ACM Symposium on Principles of Programming Languages; 1978:231–240 Tucson, AZ, January.

[GG96]Griswold Ralph E.、Griswold Madge T. Icon 编程语言。第三版,加利福尼亚州圣何塞:Peer-to-Peer Communications;1996 年已绝版;可在线访问 cs.arizona.edu/icon/lb3.htm。先前版本由 Prentice-Hall 出版。

[GG96] Griswold Ralph E., Griswold Madge T. The Icon Programming Language. third edition San Jose, CA: Peer-to-Peer Communications; 1996 Out of print; available on-line at cs.arizona.edu/icon/lb3.htm. Previous editions published by Prentice-Hall.

[国金利+03 ]Garcia Ronald、Jarvi Jaakko、Lumsdaine Andrew、Siek Jeremy、Willcock Jeremiah。通用编程语言支持的比较研究。收录于:第十八届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;2003 年 10 月,加利福尼亚州阿纳海姆,115–134 页。

[GJL+03] Garcia Ronald, Jarvi Jaakko, Lumsdaine Andrew, Siek Jeremy, Willcock Jeremiah. A comparative study of language support for generic programming. In: Proceedings of the Eighteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 2003:115–134 Anaheim, CA, October.

[加沙地带+ 14]Gosling James、Joy Bill、Steele Guy、Bracha Gilad、Buckley Alex。Java语言规范。Java SE 8 版,马萨诸塞州雷丁:Addison-Wesley;2014 年。可在docs.oracle.com/javase/specs/上获取。

[GJS+14] Gosling James, Joy Bill, Steele Guy, Bracha Gilad, Buckley Alex. The Java Language Specification. Java SE 8 edition Reading, MA: Addison-Wesley; 2014. Available at docs.oracle.com/javase/specs/.

[GL05]Guyer Samuel Z.,Lin Calvin。客户端驱动指针分析。计算机编程科学。2005;58(1–2):10 月 83–114 日。

[GL05] Guyer Samuel Z., Lin Calvin. Client-driven pointer analysis. Science of Computer Programming. 2005;58(1–2):83–114 October.

[GLDW87]Gingell Robert A.、Lee Meng、Dang Xuong T.、Weeks Mary S. SunOS 中的共享库。摘自:1987 年夏季 USENIX 会议论文集;1987:131–145 亚利桑那州凤凰城,六月。

[GLDW87] Gingell Robert A., Lee Meng, Dang Xuong T., Weeks Mary S. Shared libraries in SunOS. In: Proceedings of the 1987 Summer USENIX Conference; 1987:131–145 Phoenix, AZ, June.

[GM86]Gibbons Phillip B.、Muchnick Steven S.流水线架构的高效指令调度。摘自:SIGPLAN '86 编译器构建研讨会论文集;1986 年 7 月 11-16 日,加利福尼亚州帕洛阿尔托。

[GM86] Gibbons Phillip B., Muchnick Steven S. Efficient instruction scheduling for a pipelined architecture. In: Proceedings of the SIGPLAN '86 Symposium on Compiler Construction; 1986:11–16 Palo Alto, CA, July.

[Gol84]Goldberg Adele。Smalltalk -80:交互式编程环境。马萨诸塞州雷丁:Addison-Wesley;1984 年 Addison-Wesley 计算机科学系列。

[Gol84] Goldberg Adele. Smalltalk-80: The Interactive Programming Environment. Reading, MA: Addison-Wesley; 1984 Addison-Wesley Series in Computer Science.

[Gol91]Goldberg David。《每个计算机科学家都应该知道的浮点运算》。ACM计算调查。1991;23(1):3 月 5-48 日。

[Gol91] Goldberg David. What every computer scientist should know about floating-point arithmetic. ACM Computing Surveys. 1991;23(1):5–48 March.

[Gol12] David Goldberg。计算机算术。在 Hennessy 和 Patterson [HP12] 中,附录 J。可从booksite.mkp.com/9780123838728/references/appendix_j.pdf获取。

[Gol12] David Goldberg. Computer arithmetic. In Hennessy and Patterson [HP12], Appendix J. Available as booksite.mkp.com/9780123838728/references/appendix_j.pdf.

[Goo75]Goodenough John B. 异常处理:问题和建议的表示法。《ACM 通讯》。1975年 12 月 18(12):683-696。

[Goo75] Goodenough John B. Exception handling: Issues and a proposed notation. Communications of the ACM. 1975;18(12):683–696 December.

[Gor79]Gordon Michael JC 《编程语言的外延描述:简介》。纽约:Springer-Verlag;1979 年。

[Gor79] Gordon Michael J.C. The Denotational Description of Programming Languages: An Introduction. New York, NY: Springer-Verlag; 1979.

[GPP71]Griswold Ralph E.、Poage JF、Polonsky IP 《Snobol4 编程语言》。第二版 Englewood Cliffs,新泽西州:Prentice-Hall;1971 年。

[GPP71] Griswold Ralph E., Poage J.F., Polonsky I.P. The Snobol4 Programming Language. second edition Englewood Cliffs, NJ: Prentice-Hall; 1971.

[GR62]Ginsburg Seymour,Gordon Rice H. 与 ALGOL 相关的两种语言。《ACM 杂志》。1962;9(3):350–371。

[GR62] Ginsburg Seymour, Gordon Rice H. Two families of languages related to ALGOL. Journal of the ACM. 1962;9(3):350–371.

[89]Goldberg Adele,Robson David。Smalltalk -80:语言。马萨诸塞州雷丁:Addison-Wesley 计算机科学丛书。Addison-Wesley;1989 年。

[GR89] Goldberg Adele, Robson David. Smalltalk-80: The Language. Reading, MA: Addison-Wesley Series in Computer Science. Addison-Wesley; 1989.

[Gri81]Gries David。《编程科学》。纽约:计算机科学文本和专著。Springer-Verlag;1981 年。

[Gri81] Gries David. The Science of Programming. New York, NY: Textsand Monographs in Computer Science. Springer-Verlag; 1981.

[GS99]Gil Joseph (Yossi),Sweeney Peter F.多重继承的空间和时间高效内存布局。收录于:第十四届 ACM SIGPLAN 面向对象编程、系统、语言和应用程序会议论文集;1999:256–275 丹佛,科罗拉多州,十一月。

[GS99] Gil Joseph (Yossi), Sweeney Peter F. Space- and time-efficient memory layout for multiple inheritance. In: Proceedings of the Fourteenth ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications; 1999:256–275 Denver, CO, November.

[GSB + 93]Garrett William E.、Scott Michael L.、Bianchini Ricardo、Kontothanassis Leonidas I.、Andrew McCallum R.、Thomas Jeffrey A.、Wisniewski Robert、Luk Steve。链接共享片段。引自:USENIX 1993 年冬季技术会议论文集;1993 年 1 月 13-27 日,加利福尼亚州圣地亚哥。

[GSB+93] Garrett William E., Scott Michael L., Bianchini Ricardo, Kontothanassis Leonidas I., Andrew McCallum R., Thomas Jeffrey A., Wisniewski Robert, Luk Steve. Linking shared segments. In: Proceedings of the USENIX Winter '93 Technical Conference; 1993:13–27 San Diego, CA, January.

[GTW78]Goguen Joseph A.、Thatcher James W.、Wagner Eric G. 初步代数方法,用于规范、正确性和抽象数据类型的实现。收录于:Yeh Raymond T. 主编。Englewood Cliffs,新泽西州:Prentice-Hall;80–149。编程方法论的最新趋势。1978年;第 4 卷。

[GTW78] Goguen Joseph A., Thatcher James W., Wagner Eric G. An initial algebra approach to the specification, correctness, and implementation ofabstract data types. In: Yeh Raymond T., ed. Englewood Cliffs, NJ: Prentice-Hall; 80–149. Current Trends in Programming Methodology. 1978;volume 4.

[Gut77]Guttag John。抽象数据类型和数据结构的发展。ACM通讯。1977;20(6):396-404 六月。

[Gut77] Guttag John. Abstract data types and the development of data structures. Communications of the ACM. 1977;20(6):396–404 June.

[Hal85]Jr Robert H. Halstead。Multilisp:一种用于并发符号计算的语言。ACM编程语言和系统事务。1985;7(4):10 月 501-538 日。

[Hal85] Jr Robert H. Halstead. Multilisp: A language for concurrent symbolic computation. ACM Transactions on Programming Languages and Systems. 1985;7(4):501–538 October.

[韩81]Hanson David R. 块结构是必要的吗?。软件——实践与经验。1981;11(8):853–866 年 8 月。

[Han81] Hanson David R. Is block structure necessary?. Software—Practice and Experience. 1981;11(8):853–866 August.

[Han93] David R. Hanson。Icon 简介。载于 HOPL IIProceedings [Ass93],第 359-360 页。

[Han93] David R. Hanson. A brief introduction to Icon. In HOPL IIProceedings [Ass93], pages 359-360.

[哈92]Harbison Samuel P. Modula-3。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1992 年。

[Har92] Harbison Samuel P. Modula-3. Englewood Cliffs, NJ: Prentice-Hall; 1992.

[哈01]Harris Timothy L.非阻塞链接列表的实用实现。摘自:第十五届国际分布式计算研讨会 (DISC) 论文集;2001 年 10 月,葡萄牙里斯本,第 300–314 页。

[Har01] Harris Timothy L. A pragmatic implementation of non-blocking linked-lists. In: Proceedings of the Fifteenth International Symposium on Distributed Computing (DISC); 2001:300–314 Lisbon, Portugal, October.

[危险11]Hazelwood Kim。动态二进制修改:工具、技术和应用。Morgan & Claypool 出版社;2011 年 3 月计算机架构综合讲座。

[Haz11] Hazelwood Kim. Dynamic Binary Modification: Tools, Techniques, and Applications. Morgan & Claypool Publishers; 2011 Synthesis Lectures on Computer Architecture March.

[HD68]Hauck EA、Dent BA Burroughs 的 B6500/B7500 堆叠机制。引自:AFIPS 春季联合计算机会议论文集;1968:第 32 卷第 245-251 页,重印为 Siewiorek、Bell 和 Newell [SBN82] 的第 244-250 页。

[HD68] Hauck E.A., Dent B.A. Burroughs' B6500/B7500 stack mechanism. In: Proceedings of the AFIPS Spring Joint Computer Conference; 1968:245–251 volume 32, Reprinted as pages 244–250 of Siewiorek, Bell, and Newell [SBN82].

[HD89]Henry Robert R.,Damron Peter C.使用树形模式匹配的表驱动代码生成器算法。西雅图,华盛顿:华盛顿大学;1989 年技术报告 89-02-03,计算机科学系 2 月。

[HD89] Henry Robert R., Damron Peter C. Algorithms for table-driven code generators using tree-pattern matching. Seattle, WA: University of Washington; 1989 Technical Report 89-02-03, Computer Science Department February.

[亨14]Hendren Laurie 编辑。ACM SIGPLAN 数组编程库、语言和编译器国际研讨会。苏格兰:爱丁堡;2014 年 6 月与第三十五届 ACM SIGPLAN 编程语言设计和实现会议同时举行。

[Hen14] Hendren Laurie, ed. ACM SIGPLAN International Workshop on Libraries, Languages, and Compilers for Array Programming. Scotland: Edinburgh; 2014 June Held in conjunction with the Thirty-Fifth ACM SIGPLAN Conference on Programming Language Design and Implementation.

[Her91]Herlihy Maurice P. 无等待同步。ACM编程语言和系统事务。1991;13(1):124–149 一月。

[Her91] Herlihy Maurice P. Wait-free synchronization. ACM Transactions on Programming Languages and Systems. 1991;13(1):124–149 January.

[HGLS78]Holt Richard C.、Scott Graham G.、Lazowska Edward D.、Scott Mark A.《结构化并发编程与操作系统应用程序》。马萨诸塞州雷丁:Addison-Wesley;1978 Addison-Wesley 计算机科学系列。

[HGLS78] Holt Richard C., Scott Graham G., Lazowska Edward D., Scott Mark A. Structured Concurrent Programming with Operating Systems Applications. Reading, MA: Addison-Wesley; 1978 Addison-Wesley Series in Computer Science.

[HH97a]Hauben Michael、Hauben Ronda。《网民:Usenet 和互联网的历史与影响》。纽约州纽约市:Wiley/IEEE 计算机学会出版社;1997 年。可访问columbia.edu/~hauben/netbook/

[HH97a] Hauben Michael, Hauben Ronda. Netizens: On the History and Impact of Usenet and the Internet. New York, NY: Wiley/IEEE Computer Society Press; 1997. Available at columbia.edu/~hauben/netbook/.

[HH97b]Hookway Raymond J.,Herdeg Mark A. DIGITAL FX!32:结合仿真和二进制翻译。DIGITAL技术杂志。1997;9(1):3–12。

[HH97b] Hookway Raymond J., Herdeg Mark A. DIGITAL FX!32: Combining emulation and binary translation. DIGITAL Technical Journal. 1997;9(1):3–12.

[Hin01]Hind Michael。指针分析:我们还没有解决这个问题吗?。在:ACM SIGPLAN-SIGSOFT 软件工具和工程程序分析研讨会论文集;2001:54-61 Snowbird,犹他州,六月。

[Hin01] Hind Michael. Pointer analysis: Haven't we solved this problem yet?. In: Proceedings of the ACM SIGPLAN--SIGSOFT Workshop on Program Analysis for Software Tools and Engineering; 2001:54–61 Snowbird, UT, June.

[HJBG81]Hennessy John L.、Jouppi Norman、Baskett Forest、Gill John。MIPS :一种 VLSI 处理器架构。收录于:CMU VLSI 系统与计算会议论文集;马里兰州罗克维尔:计算机科学出版社;1981 年 10 月第 337-346 页。

[HJBG81] Hennessy John L., Jouppi Norman, Baskett Forest, Gill John. MIPS: A VLSI processor architecture. In: Proceedings of the CMU Conference on VLSI Systems and Computations; Rockville, MD: Computer Science Press; 1981:337–346 October.

[HL94]Hill Patricia M.、Lloyd John W. 《哥德尔编程语言》。马萨诸塞州剑桥:麻省理工学院出版社;1994 逻辑编程系列。

[HL94] Hill Patricia M., Lloyd John W. The Godel Programming Language. Cambridge, MA: MIT Press; 1994 Logic Programming Series.

[HLM03]Herlihy Maurice、Luchangco Victor、Moir Mark。无阻碍同步:以双端队列为例。收录于:第二十三届国际分布式计算系统会议论文集;2003 年 5 月,罗德岛州普罗维登斯,522–529。

[HLM03] Herlihy Maurice, Luchangco Victor, Moir Mark. Obstruction-free synchronization: Double-ended queues as an example. In: Proceedings of the Twenty-Third International Conference on Distributed Computing Systems; 2003:522–529 Providence, RI, May.

[HLR10]Harris Tim、Larus James R.、Rajwar Ravi。《事务性内存》。第二版 Morgan & Claypool 出版社;2010 年 12 月计算机架构综合讲座。

[HLR10] Harris Tim, Larus James R., Rajwar Ravi. Transactional Memory. second edition Morgan & Claypool Publishers; December 2010 Synthesis Lectures on Computer Architecture.

[HM93]Herlihy Maurice P.、Eliot J.、Moss B.事务内存:无锁数据结构的架构支持。收录于:第二十届国际计算机架构研讨会论文集;1993:289–300 加利福尼亚州圣地亚哥,五月。

[HM93] Herlihy Maurice P., Eliot J., Moss B. Transactional memory: Architectural support for lock-free data structures. In: Proceedings of the Twentieth International Symposium on Computer Architecture; 1993:289–300 San Diego, CA, May.

[HMPH05]Harris Tim、Marlow Simon、Jones Simon Peyton、Herlihy Maurice。可组合内存事务。收录于:第十届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2005 年:48–60 芝加哥,伊利诺斯州,六月。

[HMPH05] Harris Tim, Marlow Simon, Jones Simon Peyton, Herlihy Maurice. Composable memory transactions. In: Proceedings of the Tenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2005:48–60 Chicago, IL, June.

[HMRC88]Holt Richard C.、Matthews Philip A.、Alan Rosselet J. 和 Cordy James R. 《图灵编程语言:设计和定义》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1988 年。

[HMRC88] Holt Richard C., Matthews Philip A., Alan Rosselet J., Cordy James R. The Turing Programming Language: Design and Definition. Englewood Cliffs, NJ: Prentice-Hall; 1988.

[HMU07]Hopcroft John E.、Motwani Rajeev、Ullman Jeffrey D. 《自动机理论、语言和计算简介》。第三版 波士顿,马萨诸塞州:Pearson/Addison-Wesley;2007 年。

[HMU07] Hopcroft John E., Motwani Rajeev, Ullman Jeffrey D. Introduction to Automata Theory, Languages, and Computation. third edition Boston, MA: Pearson/Addison-Wesley; 2007.

[HO91]Wilson Ho W.,Olsson Ronald A. 真正的动态链接方法。软件实践与经验。1991;21(4):375-390 四月。

[HO91] Wilson Ho W., Olsson Ronald A. An approach to genuine dynamic linking. Software—Practice and Experience. 1991;21(4):375–390 April.

[Hoa69]Hoare Charles Antony Richard。计算机编程的公理基础。ACM通讯。1969;12(10):576–580+ 十月。

[Hoa69] Hoare Charles Antony Richard. An axiomatic basis of computer programming. Communications of the ACM. 1969;12(10):576–580+ October.

[Hoa74]Hoare Charles Antony Richard。监视器:一种操作系统结构概念。ACM通讯。1974;17(10):10 月 549-557 日。

[Hoa74] Hoare Charles Antony Richard. Monitors: An operating system structuring concept. Communications of the ACM. 1974;17(10):549–557 October.

[Hoa75]Hoare Charles Antony Richard。递归数据结构。国际计算机和信息科学杂志。1975;4(2):6 月 105-132。

[Hoa75] Hoare Charles Antony Richard. Recursive data structures. International Journal of Computer and Information Sciences. 1975;4(2):105–132 June.

[Hoa78]Hoare Charles Antony Richard。《通信顺序过程》。《ACM 通讯》。1978;21(8):8 月 666-677 日。

[Hoa78] Hoare Charles Antony Richard. Communicating Sequential Processes. Communications of the ACM. 1978;21(8):666–677 August.

[Hoa81]Hoare Charles Antony Richard。《皇帝的旧衣服》。《ACM 通讯》。1981;24(2):75-83 二月 1980 年图灵奖演讲。

[Hoa81] Hoare Charles Antony Richard. The emperor's old clothes. Communications of the ACM. 1981;24(2):75–83 February The 1980 Turing Award lecture.

[Hoa89]Hoare Charles Antony Richard。编程语言设计技巧。收录于:Jones Cliff B. 编。《计算机科学论文集》。纽约:Prentice-Hall;1989:193-216 基于 1973 年 10 月在马萨诸塞州波士顿举办的第一届 ACM 编程语言原理研讨会上发表的主题演讲。

[Hoa89] Hoare Charles Antony Richard. Hints on programming language design. In: Jones Cliff B., ed. Essays in Computing Science. New York, NY: Prentice-Hall; 1989:193–216 Based on a keynote address presented at the First ACM Symposium on Principles of Programming Languages, Boston, MA, October 1973.

[霍51]Alfred Horn。《关于代数直接并集的真句子》。《符号逻辑杂志》。1951;16(1):3 月 14-21 日。

[Hor51] Horn Alfred. On sentences which are true of direct unions of algebras. Journal of Symbolic Logic. 1951;16(1):14–21 March.

[Hor87]Horowitz Ellis。《编程语言:一次伟大的旅程》。第三版,马里兰州罗克维尔:计算机科学出版社;1987 年计算机软件工程系列。

[Hor87] Horowitz Ellis. Programming Languages: A Grand Tour. third edition Rockville, MD: Computer Science Press; 1987 Computer Software Engineering Series.

[HP12]Hennessy John L.、Patterson David A.计算机体系结构:一种定量方法。第五版旧金山,加利福尼亚州:Morgan Kaufmann;2012 第三版,2003 年。

[HP12] Hennessy John L., Patterson David A. Computer Architecture: A Quantitative Approach. fifth edition San Francisco, CA: Morgan Kaufmann; 2012 Third edition, 2003.

[12 版]Herlihy Maurice、Shavit Nir。《多处理器编程艺术》。修订版,加利福尼亚州旧金山:Morgan Kaufmann;2012 年。

[HS12] Herlihy Maurice, Shavit Nir. The Art of Multiprocessor Programming. revised edition San Francisco, CA: Morgan Kaufmann; 2012.

[HTWG11]Hejlsberg Anders、Torgersen Mads、Wiltamuth Scott 和 Golde Peter。《C# 编程语言》。第四版,马萨诸塞州波士顿:Addison-Wesley;2011 年。

[HTWG11] Hejlsberg Anders, Torgersen Mads, Wiltamuth Scott, Golde Peter. The C# Programming Language. fourth edition Boston, MA: Addison-Wesley; 2011.

[Hud89]Hudak Paul。函数式编程语言的概念、演变和应用。ACM计算调查。1989;21(3):359-411 年 9 月。

[Hud89] Hudak Paul. Conception, evolution, and application of functional programming languages. ACM Computing Surveys. 1989;21(3):359–411 September.

[IBFW91]Ichbiah Jean、Barnes John GP、Firth Robert J. 和 Woodger Mike。《Ada 编程语言设计原理》。英国剑桥:剑桥大学出版社;1991 年。

[IBFW91] Ichbiah Jean, Barnes John G.P., Firth Robert J., Woodger Mike. Rationale for the Design of the Ada Programming Language. Cambridge, England: Cambridge University Press; 1991.

[IBM87]IBM 公司。APL2编程:语言参考。1987 SH20-9227。

[IBM87] IBM Corporation. APL2 Programming: Language Reference. 1987 SH20-9227.

[IEE87]IEEE 标准委员会。IEEE 二进制浮点运算标准。ACM SIGPLAN 通知。1987;22(2):2 月 9-25 日。

[IEE87] IEEE Standards Committee. IEEE standard for binary floating-point arithmetic. ACM SIGPLAN Notices. 1987;22(2):9–25 February.

[英语61]Ingerman Peter Z. Thunks: 一种编译过程语句的方法,并对过程声明进行了一些注释。《ACM 通讯》。1961;4(1):1 月 55-58 日。

[Ing61] Ingerman Peter Z. Thunks: A way of compiling procedure statements with some comments on procedure declarations. Communications of the ACM. 1961;4(1):55–58 January.

[Ins91]电气和电子工程师协会,纽约,纽约州。IEEE /ANSI Scheme 编程语言标准。1991年。IEEE 1178-1990。可访问http://standards.ieee.org/findstds/standard/1178-1990.html

[Ins91] Institute of Electrical and Electronics Engineers, New York, NY. IEEE/ANSI Standard for the Scheme Programming Language. 1991. IEEE 1178-1990. Available at http://standards.ieee.org/findstds/standard/1178-1990.html.

[智力90]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - Pascal。1990年。ISO/IEC 7185:1990(ANSI/IEEE 770X 的修订和重新设计)。可访问pascal-central.com/docs/iso7185.pdf

[Int90] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Pascal. 1990. ISO/IEC 7185:1990 (revision and redesignation of ANSI/IEEE 770X). Available as pascal-central.com/docs/iso7185.pdf.

[Int95]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - Prolog - 第 1 部分:通用核心。1995 ISO/IEC 13211-1:1995。

[Int95] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Prolog—Part 1: General Core. 1995 ISO/IEC 13211-1:1995.

[Int96]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - 第 1 部分:Modula-2,基本语言。1996 ISO/IEC 10514-1:1996。

[Int96] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Part 1: Modula-2, Base Language. 1996 ISO/IEC 10514-1:1996.

[Int97]国际标准化组织,瑞士日内瓦。编程语言 Forth。1997 ISO/IEC 15145:1997(ANSI X3.215-1994 的修订和重新设计)。

[Int97] International Organization for Standardization, Geneva, Switzerland. Programming Language Forth. 1997 ISO/IEC 15145:1997 (revision and redesignation of ANSI X3.215–1994).

[Int99]国际标准化组织,瑞士日内瓦。编程语言 - C。1999年 12 月 ISO/IEC 9899:1999(E)。

[Int99] International Organization for Standardization, Geneva, Switzerland. Programming Language—C. 1999 December ISO/IEC 9899:1999(E).

[Int03a]国际标准化组织,瑞士日内瓦。C基本原理。2003年 4 月 ISO/IEC JTC 1/SC 22/WG 14,修订版 5.10。

[Int03a] International Organization for Standardization, Geneva, Switzerland. The C Rationale. 2003 April ISO/IEC JTC 1/SC 22/WG 14, revision 5.10.

[Int03b]国际标准化组织,瑞士日内瓦。信息技术 - 可移植操作系统接口 (POSIX)。2003年第四版。8 月 ISO/IEC 9945-1:2003。另请参阅 IEEE 标准 1003.1(2004 年版)和 The Open Group 技术标准基础规范第 6 期。可从pubs.opengroup.org/onlinepubs/009695399/获取。

[Int03b] International Organization for Standardization, Geneva, Switzerland. Information Technology—Portable Operating System Interface (POSIX). fourth edition 2003. August ISO/IEC 9945-1:2003. Also IEEE standard 1003.1, 2004 Edition, and The Open Group Technical Standard Base Specifications, Issue 6. Available at pubs.opengroup.org/onlinepubs/009695399/.

[Int10]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - Fortran - 第 1 部分:基础语言。2010 ISO/IEC 1539-1:2010。

[Int10] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Fortran—Part 1: Base Language. 2010 ISO/IEC 1539-1:2010.

[Int11]国际标准化组织,瑞士日内瓦。编程语言 - C。2011年 12 月 ISO/IEC 9899:2011。

[Int11] International Organization for Standardization, Geneva, Switzerland. Programming Languages—C. 2011 December ISO/IEC 9899:2011.

[Int12a]国际标准化组织,瑞士日内瓦。信息技术 - 通用语言基础设施 (CLI)。2012年 2 月 ISO/IEC 23271:2012。另请参阅 ECMA-335,第 6 版,2012 年 6 月。可在ecma-international.org/publications/standards/Ecma-335.htm上找到。

[Int12a] International Organization for Standardization, Geneva, Switzerland. Information Technology—Common Language Infrastructure (CLI). 2012. February ISO/IEC 23271:2012. Also ECMA-335, 6th Edition, June 2012. Available at ecma-international.org/publications/standards/Ecma-335.htm.

[Int12b]国际标准化组织,瑞士日内瓦。信息技术——编程语言——Ada。2012年 12 月 ISO/IEC 8652:2012。可在ada-auth.org/standards/ada12.html上获取。

[Int12b] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—Ada. 2012. December ISO/IEC 8652:2012. Available at ada-auth.org/standards/ada12.html.

[Int14a]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言、其环境和系统软件接口 - 编程语言 COBOL。2014 ISO/IEC 1989:2014。

[Int14a] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages, Their Environments and System Software Interfaces—Programming Language COBOL. 2014 ISO/IEC 1989:2014.

[Int14b]国际标准化组织,瑞士日内瓦。信息技术 - 编程语言 - C++。 2014 ISO/IEC 14882:2014。

[Int14b] International Organization for Standardization, Geneva, Switzerland. Information Technology—Programming Languages—C++. 2014 ISO/IEC 14882:2014.

[Int15]国际标准化组织,瑞士日内瓦。事务内存的 C++ 扩展技术规范。2015年 5 月 ISO/IEC JTC 1/SC 22/WG 21 N4514。

[Int15] International Organization for Standardization, Geneva, Switzerland. Technical Specification for C++ Extensions for Transactional Memory. 2015 May ISO/IEC JTC 1/SC 22/WG 21 N4514.

[Ive62]Iverson Kenneth E. 《一种编程语言》。纽约:John Wiley and Sons;1962 年。

[Ive62] Iverson Kenneth E. A Programming Language. New York, NY: John Wiley and Sons; 1962.

[JBW + 87]Jefferson David、Beckman Brian、Wieland Fred、Blume Leo、DiLoreto Mike、Hontalas Phil、Laroche Pierre、Sturdevant Kathy、Tupman Jack、Warren Van、Wedel John、Younger Herb、Bellenot Steve。分布式模拟和时间扭曲操作系统。摘自:第十一届 ACM 操作系统原理研讨会论文集。1987:77-93 德克萨斯州奥斯汀,11 月。

[JBW+87] Jefferson David, Beckman Brian, Wieland Fred, Blume Leo, DiLoreto Mike, Hontalas Phil, Laroche Pierre, Sturdevant Kathy, Tupman Jack, Warren Van, Wedel John, Younger Herb, Bellenot Steve. Distributed simulation and the Time Warp operating system. In: Proceedings of the Eleventh ACM Symposium on Operating Systems Principles. 1987:77–93 Austin, TX, November.

[JF10]Järvi Jaakko,Freeman John。C++ lambda 表达式和闭包。计算机编程科学。2010;75(9):9 月 762-772。

[JF10] Järvi Jaakko, Freeman John. C++ lambda expressions and closures. Science of Computer Programming. 2010;75(9):762–772 September.

[JG89]Jones Geraint、Goldsmith Michael。Occam2编程。第二版 Englewood Cliffs,新泽西州:Prentice-Hall;1989 年 Prentice-Hall 国际计算机科学丛书。

[JG89] Jones Geraint, Goldsmith Michael. Programming in occam2. second edition Englewood Cliffs, NJ: Prentice-Hall; 1989 Prentice-Hall International Series in Computer Science.

[JGF96]Jones Simon Peyton、Gordon Andrew、Finne Sigbjorn。并发 Haskell。收录于:第二十三届 ACM 编程语言原理研讨会论文集;1996:295–308 圣彼得堡海滩,佛罗里达州,一月。

[JGF96] Jones Simon Peyton, Gordon Andrew, Finne Sigbjorn. Concurrent Haskell. In: Proceedings of the Twenty-Third ACM Symposium on Principles of Programming Languages; 1996:295–308 St. Petersburg Beach, FL, January.

[JL96]Jones Richard、Lins Rafael。垃圾收集:自动动态内存管理算法。纽约:John Wiley and Sons;1996 年。

[JL96] Jones Richard, Lins Rafael. Garbage Collection: Algorithms for Automatic Dynamic Memory Management. New York, NY: John Wiley and Sons; 1996.

[JM94]Jaffar Joxan,Maher Michael J. 约束逻辑编程:调查。《逻辑编程杂志》。1994;20:503–581 五月至七月。

[JM94] Jaffar Joxan, Maher Michael J. Constraint logic programming: A survey. Journal of Logic Programming. 1994;20:503–581 May–July.

[约翰福音75]Johnson Stephen C. Yacc—又一个编译器。新泽西州 Murray Hill:AT&T Bell 实验室;1975 年技术报告 32,计算机科学。

[Joh75] Johnson Stephen C. Yacc—Yet another compiler compiler. Murray Hill, NJ: AT&T Bell Laboratories; 1975 Technical Report 32, Computing Science.

[JOR75]Jazayeri Mehdi、Ogden William F.、Rounds William C. 属性语法循环问题的内在指数复杂度。ACM通讯。1975年 12 月 18(12):697-706。

[JOR75] Jazayeri Mehdi, Ogden William F., Rounds William C. The intrinsically exponential complexity of the circularity problem for attribute grammars. Communications of the ACM. 1975;18(12):697–706 December.

[JPAR68]Johnson Walter L.,Porter James H.,Ackley Stephanie I.,Ross Douglas T. 使用有限状态技术自动生成高效词汇处理器。ACM通讯。1968;11(12):805-813 十二月。

[JPAR68] Johnson Walter L., Porter James H., Ackley Stephanie I., Ross Douglas T. Automatic generation of efficient lexical processors using finite state techniques. Communications of the ACM. 1968;11(12):805–813 December.

[JW91]Jensen Kathleen、Wirth Niklaus。Pascal用户手册和报告:ISO Pascal 标准。第四版纽约:Springer-Verlag;1991 年由 Andrew B. Mickel 和 James F. Miner 修订。ISBN 0-387-97649-3。

[JW91] Jensen Kathleen, Wirth Niklaus. Pascal User Manual and Report: ISO Pascal Standard. fourth edition New York, NY: Springer-Verlag; 1991 Revised by Andrew B. Mickel and James F. Miner. ISBN 0-387-97649-3.

[卡斯65]Kasami T.一种高效的上下文无关语言识别和语法分析算法。马萨诸塞州贝德福德:空军剑桥研究实验室;1965 年技术报告 AFCRL-65-758。

[Kas65] Kasami T. An efficient recognition and syntax analysis algorithm for context-free languages. Bedford, MA: Air Force Cambridge Research Laboratory; 1965 Technical Report AFCRL-65-758.

[九铁+ 98]Kelsey Richard、Clinger William、Rees Jonathan、Abelson H.、Dybvig RK、Haynes CT、Rozas GJ、Adams NI IV、Friedman DP、Kohlbecker E.、Steele GL Jr.、Bartley DH、Halstead R.、Oxley D.、Sussman GJ、Brooks G.、Hanson C.、Pitman KM、Wand M.算法语言 Scheme 的修订报告5。高阶与符号计算。1998年。;11(1):7–105。由 Kelsey、Clinger 和 Rees 编辑。可在schemers.org/Documents/Standards/R5RS/上获取。

[KCR+98] Kelsey Richard, Clinger William, Rees Jonathan, Abelson H., Dybvig R.K., Haynes C.T., Rozas G.J., Adams N.I. IV, Friedman D.P., Kohlbecker E., Steele G.L. Jr., Bartley D.H., Halstead R., Oxley D., Sussman G.J., Brooks G., Hanson C., Pitman K.M., Wand M. Revised5 report on the algorithmic language Scheme. Higher-Order and Symbolic Computation. 1998. ;11(1):7–105. Edited by Kelsey, Clinger, and Rees. Available at schemers.org/Documents/Standards/R5RS/.

[Kee89]Keene Sonya E. Common Lisp 中的面向对象编程:CLOS 程序员指南。马萨诸塞州雷丁:Addison-Wesley;1989 年 Dan Gerson 供稿。

[Kee89] Keene Sonya E. Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS. Reading, MA: Addison-Wesley; 1989 Contributions by Dan Gerson.

[Kep04]Kepser Stephan。XSLT和 XQuery 图灵完备性的简单证明。收录于:2004 年极端标记语言会议论文集,加拿大蒙特利尔;2004 年 8 月可从conferences.idealliance.org/extreme/html/2004/Kepser01/EML2004Kepser01.html获取。

[Kep04] Kepser Stephan. A simple proof for the Turing-completeness of XSLT and XQuery. In: Proceedings, Extreme Markup Languages 2004, Montréal, Canada; 2004. August Available as conferences.idealliance.org/extreme/html/2004/Kepser01/EML2004Kepser01.html.

[Ker81]Kernighan Brian W.为什么 Pascal 不是我最喜欢的编程语言。Murray Hill,新泽西州:计算机科学,AT&T 贝尔实验室;1981 年技术报告 100 重印为 Feuer 和 Gehani [FG84] 的第 170-186 页。

[Ker81] Kernighan Brian W. Why Pascal is not my favorite programming language. Murray Hill, NJ: Computing Science, AT&T Bell Laboratories; 1981 Technical Report 100 Reprinted as pages 170–186 of Feuer and Gehani [FG84].

[77 卡斯]Kessels Joep LW 监视器中用于同步的事件队列的替代方案。《ACM 通讯》。1977;20(7):7 月 500-503。

[Kes77] Kessels Joep L.W. An alternative to event queues for synchronization in monitors. Communications of the ACM. 1977;20(7):500–503 July.

[克莱56]Kleene Stephen C. 神经网络和有限自动机中的事件表示。收录于:Shannon Claude E.、McCarthy John 编。《自动机研究》。新泽西州普林斯顿:普林斯顿大学出版社;1956 年:3-41,《数学研究年鉴》第 34 期。

[Kle56] Kleene Stephen C. Representation ofevents in nerve nets and finite automata. In: Shannon Claude E., McCarthy John, eds. Automata Studies. Princeton, NJ: Princeton University Press; 1956:3–41 number 34 in Annals of Mathematical Studies.

[KMP77]Knuth Donald E.、Morris James H.、Pratt Vaughan R. 字符串中的快速模式匹配。SIAM计算杂志。1977;6(2):6 月 323-350。

[KMP77] Knuth Donald E., Morris James H., Pratt Vaughan R. Fast pattern matching in strings. SIAM Journal of Computing. 1977;6(2):323–350 June.

[Knu65]Knuth Donald E. 论从左到右的语言翻译。信息与控制。1965;8(6):607–639 十二月。

[Knu65] Knuth Donald E. On the translation of languages from left to right. Information and Control. 1965;8(6):607–639 December.

[Knu68]Knuth Donald E. 上下文无关语言的语义。数学系统理论。1968;2(2):127–145 六月更正见第 5 卷第 95–96 页。

[Knu68] Knuth Donald E. Semantics of context-free languages. Mathematical Systems Theory. 1968;2(2):127–145 June Correction appears in Volume 5, pages 95–96.

[Knu84]Knuth Donald E. 文学编程。计算机杂志。1984;27(2):97-111 月。

[Knu84] Knuth Donald E. Literate programming. The Computer Journal. 1984;27(2):97–111 May.

[Knu86]Knuth Donald E. The TeXbook。马萨诸塞州雷丁:Addison-Wesley;1986 年。

[Knu86] Knuth Donald E. The TeXbook. Reading, MA: Addison-Wesley; 1986.

[Kor94]Korn David G. ksh:一种可扩展的高级语言。摘自:USENIX 超高级语言研讨会论文集;1994:129–146 新墨西哥州圣达菲,十月。

[Kor94] Korn David G. ksh: An extensible high level language. In: Proceedings of the USENIX Very High Level Languages Symposium; 1994:129–146 Santa Fe, NM, October.

[第 78 页]Kernighan Brian W.、Plauger Phillip J.《编程风格的要素》。第二版,纽约:McGraw-Hill;1978 年。

[KP78] Kernighan Brian W., Plauger Phillip J. The Elements of Programming Style. second edition New York, NY: McGraw-Hill; 1978.

[KR88]Kernighan Brian W.、Ritchie Dennis M. 《C 编程语言》。第二版 Englewood Cliffs,新泽西州:Prentice-Hall;1988 年。

[KR88] Kernighan Brian W., Ritchie Dennis M. The C Programming Language. second edition Englewood Cliffs, NJ: Prentice-Hall; 1988.

[克拉73]Král Jaroslav。模式的等价性和有限自动机的等价性。ALGOL Bulletin。1973;35:34-35 月。

[Krá73] Král Jaroslav. The equivalence of modes and the equivalence of finite automata. ALGOL Bulletin. 1973;35:34–35 March.

[KS01]Kennedy Andrew、Syme Don。.NET通用语言运行时的泛型设计和实现。摘自:SIGPLAN 2001 编程语言设计和实现会议论文集;2001 年 6 月,犹他州 Snowbird,1-12。

[KS01] Kennedy Andrew, Syme Don. Design and implementation of generics for the .NET Common Language Runtime. In: Proceedings of the SIGPLAN 2001 Conference on Programming Language Design and Implementation; 2001:1–12 Snowbird, UT, June.

[KWS97]Kontothanassis Leonidas I.、Wisniewski Robert、Scott Michael L. 调度程序意识同步。ACM计算机系统学报。1997;15(1):2 月 3 日到 40 日。

[KWS97] Kontothanassis Leonidas I., Wisniewski Robert, Scott Michael L. Scheduler-conscious synchronization. ACM Transactions on Computer Systems. 1997;15(1):3–40 February.

[Lam78]Lamport Leslie。分布式系统中的时间、时钟和事件排序。ACM通讯。1978;21(7):7 月 558-565。

[Lam78] Lamport Leslie. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM. 1978;21(7):558–565 July.

[Lam87]Lamport Leslie。一种快速互斥算法。ACM计算机系统学报。1987;5(1):2 月 1 日 - 11 日。

[Lam87] Lamport Leslie. A fast mutual exclusion algorithm. ACM Transactions on Computer Systems. 1987;5(1):1–11 February.

[Lam94]Lamport Leslie。LaTeX :文档准备系统。第二版,马萨诸塞州雷丁:Addison-Wesley Professional;1994 年。

[Lam94] Lamport Leslie. LaTeX: A Document Preparation System. second edition Reading, MA: Addison-Wesley Professional; 1994.

[Lam05]Lameter Christoph。Linux /NUMA 系统上的有效同步。收录于:Gelato 联盟会议论文集,加利福尼亚州圣何塞;2005 年 5 月。

[Lam05] Lameter Christoph. Effective synchronization on Linux/NUMA systems. In: In Proceedings of the Gelato Federation Meeting, San Jose, CA; 2005 May.

[液晶显示模组+05 ]Luk Chi-Keung、Cohn Robert、Muth Robert、Patil Harish、Klauser Artur、Lowney Geoff、Wallace Steven、Reddi Vijay Janapa、Hazelwood Kim。Pin :使用动态检测构建自定义程序分析工具。在:SIGPLAN 2005 编程语言设计和实现会议论文集;2005:190-200 芝加哥,伊利诺伊州,六月。

[LCM+05] Luk Chi-Keung, Cohn Robert, Muth Robert, Patil Harish, Klauser Artur, Lowney Geoff, Wallace Steven, Reddi Vijay Janapa, Hazelwood Kim. Pin: Building customized program analysis tools with dynamic instrumentation. In: Proceedings of the SIGPLAN 2005 Conference on Programming Language Design and Implementation; 2005:190–200 Chicago, IL, June.

[Lee06]Lee Edward A. 线程问题。计算机。2006;39(5):5 月 33-42 日。

[Lee06] Lee Edward A. The problem with threads. Computer. 2006;39(5):33–42 May.

[莱斯75]Lesk Michael E. Lex—词汇分析器生成器。新泽西州 Murray Hill:计算机科学,AT&T 贝尔实验室;1975 年技术报告 39。

[Les75] Lesk Michael E. Lex—A lexical analyzer generator. Murray Hill, NJ: Computing Science, AT&T Bell Laboratories; 1975 Technical Report 39.

[LG86]Barbara Liskov,John Guttag。《程序开发中的抽象与规范》。马萨诸塞州剑桥:麻省理工学院出版社;1986 年。

[LG86] Liskov Barbara, Guttag John. Abstraction and Specification in Program Development. Cambridge, MA: MIT Press; 1986.

[LH89]Li Kai, Hudak Paul。共享虚拟内存系统中的内存一致性。ACM计算机系统学报。1989;7(4):321-359 年 11 月。

[LH89] Li Kai, Hudak Paul. Memory coherence in shared virtual memory systems. ACM Transactions on Computer Systems. 1989;7(4):321–359 November.

[左手边+ 77]Lampson Butler W.、Horning JJ、London RL、Mitchell JG、Popek GJ 关于编程语言 Euclid 的报告。ACM SIGPLAN 通知。1977;12(2):2 月 1-79 日。

[LHL+77] Lampson Butler W., Horning J.J., London R.L., Mitchell J.G., Popek G.J. Report on the programming language Euclid. ACM SIGPLAN Notices. 1977;12(2):1–79 February.

[LK08]Larus James,Kozyrakis Christos。事务性内存。ACM通讯。2008;51(7):7 月 80-88 日。

[LK08] Larus James, Kozyrakis Christos. Transactional memory. Communications of the ACM. 2008;51(7):80–88 July.

[LL12]Louden Kenneth C.、Lambert Kenneth A.编程语言:原理与实践。第三版,马萨诸塞州波士顿:Cengage Learning;2012 年。

[LL12] Louden Kenneth C., Lambert Kenneth A. Programming Languages: Principles and Practice. third edition Boston, MA: Cengage Learning; 2012.

[Llo87]Lloyd John W. 《逻辑编程基础》。第二版柏林,西德:Springer-Verlag;1987 年。

[Llo87] Lloyd John W. Foundations of Logic Programming. second edition Berlin, West Germany: Springer-Verlag; 1987.

[洛姆75]Lomet David B. 使对释放存储的引用无效的方案。IBM研究与开发杂志。1975;19(1):1 月 26-35 日。

[Lom75] Lomet David B. Scheme for invalidating references to freed storage. IBM Journal of Research and Development. 1975;19(1):26–35 January.

[Lom85]Lomet David B. 在系统编程语言中确保指针安全。IEEE软件工程学报。1985;SE–11(1):87–96 年 1 月。

[Lom85] Lomet David B. Making pointers safe in system programming languages. IEEE Transactions on Software Engineering. 1985;SE–11(1):87–96 January.

[LP80]Luckam David C.,Polak W. Ada 异常处理:一种公理方法。ACM编程语言和系统事务。1980;2(2):4 月 225-233。

[LP80] Luckam David C., Polak W. Ada exception handling: An axiomatic approach. ACM Transactions on Programming Languages and Systems. 1980;2(2):225–233 April.

[LR80]Lampson Butler W.,Redell David D. Mesa 中的进程和监视器经验。ACM通讯。1980;23(2):2 月 105-117。

[LR80] Lampson Butler W., Redell David D. Experience with processes and monitors in Mesa. Communications of the ACM. 1980;23(2):105–117 February.

[LRS74]Lewis Philip M. II、Rosenkrantz Daniel J.、Stearns Richard E. 署名翻译。《计算机与系统科学杂志》。1974;9(3):279–307 年 12 月。

[LRS74] Lewis Philip M. II, Rosenkrantz Daniel J., Stearns Richard E. Attributed translations. Journal of Computer and System Sciences. 1974;9(3):279–307 December.

[LS68]Lewis Philip M. II、Stearns Richard E. 句法导向转导。《ACM 杂志》。1968;15(3):465–488 年 7 月。

[LS68] Lewis Philip M. II, Stearns Richard E. Syntax-directed transduction. Journal of the ACM. 1968;15(3):465–488 July.

[LS79]Barbara Liskov,Alan Snyder。CLU 中的异常处理。IEEE软件工程学报。1979;SE–5(6):546–558 十一月。

[LS79] Liskov Barbara, Snyder Alan. Exception handling in CLU. IEEE Transactions on Software Engineering. 1979;SE–5(6):546–558 November.

[LS83]Barbara Liskov,Robert Scheifler。《守护者与行动:对稳健分布式程序的语言支持》。《ACM 编程语言与系统汇刊》。1983;5(3):7 月 381-404。

[LS83] Liskov Barbara, Scheifler Robert. Guardians and actions: Linguistic support for robust, distributed programs. ACM Transactions on Programming Languages and Systems. 1983;5(3):381–404 July.

[LSAS77]Barbara Liskov、Alan Snyder、Atkinson Russel、Craig Schaffert J. CLU 中的抽象机制。ACM通讯。1977;20(8):8 月 564-576 日。

[LSAS77] Liskov Barbara, Snyder Alan, Atkinson Russel, Craig Schaffert J. Abstraction mechanisms in CLU. Communications of the ACM. 1977;20(8):564–576 August.

[LYBB14]Lindholm Tim、Yellin Frank、Bracha Gilad、Buckley Alex。Java虚拟机规范。Java SE 8 版 Addison-Wesley Professional;2014 年。可在docs.oracle.com/javase/specs/上获取。

[LYBB14] Lindholm Tim, Yellin Frank, Bracha Gilad, Buckley Alex. The Java Virtual Machine Specification. Java SE 8 edition Addison-Wesley Professional; 2014. Available at docs.oracle.com/javase/specs/.

[Mac77]Donald MacLaren M. PL/I 中的异常处理。收录于:Wortman David B. 编辑。ACM 可靠软件语言设计会议论文集;1977 年:101–104 北卡罗来纳州罗利市。

[Mac77] Donald MacLaren M. Exception handling in PL/I. In: Wortman David B., ed. Proceedings of an ACM Conference on Language Design for Reliable Software; 1977:101–104 Raleigh, NC.

[MAE + 65]McCarthy John、Abrahams Paul W.、Edwards Daniel J.、Hart Timothy P.、Levin Michael I. LISP 1.5 程序员手册。第二版马萨诸塞州剑桥:麻省理工学院出版社;1965 年可作为 softwarepreservation.org/projects/LISP/book/LISP%​​201.5%20Programmers%20Manual.pdf 获得。

[MAE+65] McCarthy John, Abrahams Paul W., Edwards Daniel J., Hart Timothy P., Levin Michael I. LISP 1.5 Programmer's Manual. second edition Cambridge, MA: MIT Press; 1965 Available as softwarepreservation.org/projects/LISP/book/LISP%201.5%20Programmers%20Manual.pdf.

[麦90]Mairson Harry G.确定 ML 可类型化性在确定性指数时间内完成。在:第十七届 ACM 编程语言原理研讨会会议记录;1990:382–401 旧金山,加利福尼亚州,一月。

[Mai90] Mairson Harry G. Deciding ML typability is complete for deterministic exponential time. In: Conference Record of the Seventeenth Annual ACM Symposium on Principles of Programming Languages; 1990:382–401 San Francisco, CA, January.

[MAK + 01]麦肯尼·保罗·E.、阿帕沃·乔纳森、克莱恩·安迪、克里格·奥兰、拉塞尔·鲁斯蒂、萨尔玛·迪潘卡、索尼·曼尼什。读取复制更新。见:渥太华 Linux 研讨会论文集; 2001:338–367 加拿大安大略省渥太华,七月。

[MAK+01] McKenney Paul E., Appavoo Jonathan, Kleen Andi, Krieger Orran, Russel Rusty, Sarma Dipankar, Soni Maneesh. Read-copy update. In: Proceedings of the Ottawa Linux Symposium; 2001:338–367 Ottawa, ON, Canada, July.

[马斯76]Mashey John R.使用命令语言作为高级编程语言。引自:第二届国际 IEEE 软件工程大会论文集;1976:169–176 旧金山,加利福尼亚州,十月。

[Mas76] Mashey John R. Using a command language as a high-level programming language. In: Proceedings of the Second International IEEE Conference on Software Engineering; 1976:169–176 San Francisco, CA, October.

[马斯87]Massalin Henry。超级优化器:看看最小的程序。在:第二届编程语言和操作系统架构支持国际会议论文集;1987:122-126 帕洛阿尔托,加利福尼亚州,十月。

[Mas87] Massalin Henry. Superoptimizer: A look at the smallest program. In: Proceedings of the Second International Conference on Architectural Support for Programming Languages and Operating Systems; 1987:122–126 Palo Alto, CA, October.

[McC60]McCarthy John。符号表达式的递归函数及其机器计算,第一部分。ACM通讯。1960;3(4):4 月 184-195 日。

[McC60] McCarthy John. Recursive functions of symbolic expressions and their computation by machine, Part I. Communications of the ACM. 1960;3(4):184–195 April.

[麦克格82]McGraw James R. VAL 语言:描述和分析。ACM编程语言和系统事务。1982;4(1):44-82 年 1 月。

[McG82] McGraw James R. The VAL language: Description and analysis. ACM Transactions on Programming Languages and Systems. 1982;4(1):44–82 January.

[麦克K04]McKinley Kathryn S. 编辑。ACM SIGPLAN 编程语言设计和实现会议 20 周年,1979-1999。纽约:ACM Press;2004 年,另见 ACM SIGPLAN 通知,39(4),2004 年 4 月。

[McK04] McKinley Kathryn S., ed. 20 Years of the ACM SIGPLAN Conference on Programming Language Design and Implementation, 1979–1999. New York, NY: ACM Press; 2004 Also ACM SIGPLAN Notices, 39(4), April 2004.

[MCS91]Mellor-Crummey John M.,Scott Michael L. 共享内存多处理器上的可扩展同步算法。ACM计算机系统学报。1991;9(1):2 月 21-65 日。

[MCS91] Mellor-Crummey John M., Scott Michael L. Algorithms for scalable synchronization on shared-memory multiprocessors. ACM Transactions on Computer Systems. 1991;9(1):21–65 February.

[Mes12]消息传递接口论坛。MPI :消息传递接口标准。2012年 9 月,版本 3.0。可从mpi-forum.org/docs/mpi-3.0/mpi30-report.pdf获取。

[Mes12] Message Passing Interface Forum. MPI: A Message-Passing Interface Standard. 2012. September Version 3.0. Available as mpi-forum.org/docs/mpi-3.0/mpi30-report.pdf.

[Mey92a]Meyer Bertrand。应用“契约式设计”。IEEE计算机。1992;25(10):10 月 40-51 日。

[Mey92a] Meyer Bertrand. Applying "design by contract". IEEE Computer. 1992;25(10):40–51 October.

[Mey92b]Meyer Bertrand。《埃菲尔:语言》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1992 年。

[Mey92b] Meyer Bertrand. Eiffel: The Language. Englewood Cliffs, NJ: Prentice-Hall; 1992.

[MF08]Matthews Jacob,Findel Robert Bruce。Scheme 的操作语义。函数式编程杂志。2008;18(1):47-86。

[MF08] Matthews Jacob, Findler Robert Bruce. An operational semantics for Scheme. Journal of Functional Programming. 2008;18(1):47–86.

[MGA92]Martonosi Margaret、Gupta Anoop、Anderson Thomas。MemSpy :分析程序中的内存系统瓶颈。收录于:1992 年 ACM SIGMETRICS 计算机系统测量与建模联合国际会议论文集;1992 年:1-12 纽波特,罗德岛州,六月。

[MGA92] Martonosi Margaret, Gupta Anoop, Anderson Thomas. MemSpy: Analyzing memory system bottlenecks in programs. In: Proceedings of the 1992 ACM SIGMETRICS Joint International Conference on Measurement and Modeling of Computer Systems; 1992:1–12 Newport, RI, June.

[麦克风68]Michie Donald。“备忘录”功能和机器学习。《自然》。1968;218(5136):4 月 19-22 日。

[Mic68] Michie Donald. ‘Memo’ functions and machine learning. Nature. 1968;218(5136):19–22 April.

[麦克风89]Michaelson Greg。通过 Lambda 演算介绍函数式编程。英国沃金厄姆:Addison-Wesley;1989 年国际计算机科学系列。

[Mic89] Michaelson Greg. An Introduction to Functional Programming through Lambda Calculus. Wokingham, England: Addison-Wesley; 1989 International Computer Science Series.

[麦克风12]微软公司。C # 语言规范。2012年。版本 5.0 可在msdn.microsoft.com/en-us/library/ms228593.aspx上获取。

[Mic12] Microsoft Corporation. C# Language Specification. 2012. Version 5.0 Available at msdn.microsoft.com/en-us/library/ms228593.aspx.

[米尔78]Milner Robin。编程中的类型多态性理论。计算机与系统科学杂志。1978;17(3):12 月 348-375。

[Mil78] Milner Robin. A theory of type polymorphism in programming. Journal of Computer and System Sciences. 1978;17(3):348–375 December.

[MKH91]Mohr Eric、Kranz David A.、Jr Robert H. Halstead。惰性任务创建:一种提高并行程序粒度的技术。IEEE并行和分布式系统学报。1991;2(3):7 月 264-280。

[MKH91] Mohr Eric, Kranz David A., Jr Robert H. Halstead. Lazy task creation: A technique for increasing the granularity of parallel programs. IEEE Transactions on Parallel and Distributed Systems. 1991;2(3):264–280 July.

[美国职棒大联盟76]Marcotty Michael、Ledgard Henry F.、Bochmann Gregor V. 正式定义样本。ACM计算调查。1976;8(2):191-276 年 6 月。

[MLB76] Marcotty Michael, Ledgard Henry F., Bochmann Gregor V. A sampler of formal definitions. ACM Computing Surveys. 1976;8(2):191–276 June.

[MM08]Marathe Virendra J.,Moir Mark。迈向高性能非阻塞软件事务内存。收录于:第十三届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2008 年:227–236 犹他州盐湖城,二月。

[MM08] Marathe Virendra J., Moir Mark. Toward high performance nonblocking software transactional memory. In: Proceedings of the Thirteenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2008:227–236 Salt Lake City, UT, February.

[Moo78]Moon David A. MacLisp 参考手册。麻省理工学院人工智能实验室;1978 年。

[Moo78] Moon David A. MacLisp Reference Manual. MIT Artificial Intelligence Laboratory; 1978.

[Moo86]Moon David A.具有 Flavors 的面向对象编程。引自:OOPSLA '86 会议论文集:面向对象编程系统、语言和应用程序;1986 年 9 月,波特兰,1-8。

[Moo86] Moon David A. Object-oriented programming with Flavors. In: OOPSLA '86 Conference Proceedings: Object-Oriented Programming Systems, Languages, and Applications; 1986:1–8 Portland, OR, September.

[Mor70]Morgan Howard L. 系统程序中的拼写更正。《ACM 通讯》。1970;13(2):90–94。

[Mor70] Morgan Howard L. Spelling correction in systems programs. Communications of the ACM. 1970;13(2):90–94.

[MOSS96]Murer Stephan、Omohundro Stephen、Stoutamire David、Szyperski Clemens。Sather 中的迭代抽象。ACM编程语言和系统事务。1996;18(1):1 月 1 日 - 15 日。

[MOSS96] Murer Stephan, Omohundro Stephen, Stoutamire David, Szyperski Clemens. Iteration abstraction in Sather. ACM Transactions on Programming Languages and Systems. 1996;18(1):1–15 January.

[MPA05]Manson Jeremy、Pugh William、Adve Sarita V. Java 内存模型。摘自:第三十二届 ACM 编程语言原理研讨会论文集;2005:378–391 加州长滩,1 月。

[MPA05] Manson Jeremy, Pugh William, Adve Sarita V. The Java memory model. In: Proceedings of the Thirty-Second ACM Symposium on Principles of Programming Languages; 2005:378–391 Long Beach, CA, January.

[MR96]Metcalf Michael,Reid John。《Fortran 90/95 解析》。英国伦敦:牛津大学出版社;1996 年。

[MR96] Metcalf Michael, Reid John. Fortran 90/95 Explained. London, England: Oxford University Press; 1996.

[MR04]Miller James S.、Ragsdale Susann。《通用语言基础结构注释标准》。马萨诸塞州波士顿:Addison-Wesley;2004 年基于 ECMA-335,第 2 版,2002 年。

[MR04] Miller James S., Ragsdale Susann. The Common Language Infrastructure Annotated Standard. Boston, MA: Addison-Wesley; 2004 Based on ECMA-335, 2nd Edition, 2002.

[MS96]Michael Maged M.,Scott Michael L.简单、快速、实用的非阻塞和阻塞并发队列算法。摘自:第十五届 ACM 分布式计算原理研讨会论文集;1996:267–275 宾夕法尼亚州费城,五月。

[MS96] Michael Maged M., Scott Michael L. Simple, fast, and practical non-blocking and blocking concurrent queue algorithms. In: Proceedings of the Fifteenth Annual ACM Symposium on Principles of Distributed Computing; 1996:267–275 Philadelphia, PA, May.

[MS98]Michael Maged M.,Scott Michael L. 多道程序共享内存多处理器上的非阻塞算法和抢占安全锁定。并行和分布式计算杂志。1998;51:1-26。

[MS98] Michael Maged M., Scott Michael L. Nonblocking algorithms and preemption-safe locking on multiprogrammed shared memory multiprocessors. Journal of Parallel and Distributed Computing. 1998;51:1–26.

[MTAB13]Meyerovich Leo A.、Torok Matthew E.、Atkinson Eric、Bodik Rastislav。属性语法的并行调度合成。收录于:第十八届 ACM SIGPLAN 并行编程原理与实践研讨会论文集;2013 年 2 月,中国深圳,187–196。

[MTAB13] Meyerovich Leo A., Torok Matthew E., Atkinson Eric, Bodik Rastislav. Parallel schedule synthesis for attribute grammars. In: Proceedings of the Eighteenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming; 2013:187–196 Shenzhen, China, February.

[MTHM97]Milner Robin、Tofte Mads、Harper Robert 和 David MacQueen。《标准 ML 的定义——修订版》。马萨诸塞州剑桥:麻省理工学院出版社;1997 年。

[MTHM97] Milner Robin, Tofte Mads, Harper Robert, MacQueen David. The Definition of Standard ML—Revised. Cambridge, MA: MIT Press; 1997.

[Muc97]Muchnick Steven S.高级编译器设计和实现。旧金山,加州:Morgan Kaufmann;1997 年。

[Muc97] Muchnick Steven S. Advanced Compiler Design and Implementation. San Francisco, CA: Morgan Kaufmann; 1997.

[95 马币]McKenzie Bruce J.、Yeatman Corey、De Vere Lorraine。《移位归约解析器中的错误修复》。《ACM 编程语言和系统汇刊》。1995;17(4):7 月 672-689 日。

[MYD95] McKenzie Bruce J., Yeatman Corey, De Vere Lorraine. Error repair in shift-reduce parsers. ACM Transactions on Programming Languages and Systems. 1995;17(4):672–689 July.

[NA01]尼基尔·里希尤尔·S.,阿尔文德。pH 中的隐式并行编程。加利福尼亚州旧金山:摩根·考夫曼; 2001年。

[NA01] Nikhil Rishiyur S., Arvind. Implicit Parallel Programming in pH. San Francisco, CA: Morgan Kaufmann; 2001.

[NBB + 63]Peter Naur (ed.) Backus JW、Bauer FL、Green J.、Katz C.、McCarthy J.、Perlis AJ、Rutishauser H.、Samelson K.、Vauquois B.、Wegstein JH、van Wijngaarden A. 和 Woodger M. 算法语言 ALGOL 60 的修订报告。ACM通讯。1963;6(1):1-1 月 23 日 原始版本出现在 1960 年 5 月刊中。

[NBB+63] Peter Naur (ed.) Backus J.W., Bauer F.L., Green J., Katz C., McCarthy J., Perlis A.J., Rutishauser H., Samelson K., Vauquois B., Wegstein J.H., van Wijngaarden A., Woodger M. Revised report on the algorithmic language ALGOL 60. Communications of the ACM. 1963;6(1):1–23 January Original version appeared in the May 1960 issue.

[ND78] Kristen Nygaard 和 Ole-Johan Dahl。《Simula 语言的发展》。《HOPL I 论文集》[Wex78],第 439-493 页。

[ND78] Kristen Nygaard and Ole-Johan Dahl. The development of the Simula languages. In HOPL I Proceedings [Wex78], pages 439-493.

[Nec97]Necula George C.带有证明的代码。出处:第二十四届 ACM 编程语言原理研讨会会议记录;1997 年 1 月,法国巴黎,第 106-119 页。

[Nec97] Necula George C. Proof-carrying code. In: Conference Record of the Twenty-Fourth ACM Symposium on Principles of Programming Languages; 1997:106–119 Paris, France, January.

[Nel65]纳尔逊·西奥多·霍尔姆。复杂信息处理:复杂、变化和不确定的文件结构。收录于:第二十届 ACM 全国大会论文集;1965 年 84-100 页,俄亥俄州克利夫兰,八月。

[Nel65] Nelson Theodor Holm. Complex information processing: A file structure for the complex, the changing, and the indeterminate. In: Proceedings of the Twentieth ACM National Conference; 1965 pages 84-100, Cleveland, OH, August.

[NK15]Nipkow Tobias、Klein Gerwin。《Isabelle/HOL 的具体语义学》。德国柏林:Springer Science+Business Media;2015 年。

[NK15] Nipkow Tobias, Klein Gerwin. Concrete Semantics with Isabelle/HOL. Berlin, Germany: Springer Science+Business Media; 2015.

[Ous82]Ousterhout John K.并发系统的调度技术。在:第三届国际分布式计算系统会议论文集;1982:22-30 迈阿密/劳德代尔堡,佛罗里达州,十月。

[Ous82] Ousterhout John K. Scheduling techniques for concurrent systems. In: Proceedings of the Third International Conference on Distributed Computing Systems; 1982:22–30 Miami/Ft. Lauderdale, FL, October.

[Ous94]Ousterhout John K. Tcl 和 Tk 工具包。马萨诸塞州雷丁:Addison-Wesley Professional;1994 年。

[Ous94] Ousterhout John K. Tcl and the Tk Toolkit. Reading, MA: Addison-Wesley Professional; 1994.

[Ous98]Ousterhout John K. 脚本:面向 21 世纪的高级编程。IEEE计算机。1998;31(3):3 月 23-30 日。

[Ous98] Ousterhout John K. Scripting: Higher-level programming for the 21st century. IEEE Computer. 1998;31(3):23–30 March.

[第76页]Pagan Frank G. 《Algol 68 实用指南》。英国伦敦:John Wiley and Sons;1976 年。

[Pag76] Pagan Frank G. A Practical Guide to Algol 68. London, England: John Wiley and Sons; 1976.

[标准杆72]Parnas David L. 论将系统分解为模块时应使用的标准。ACM通讯。1972;15(12):1053-1058 年 12 月。

[Par72] Parnas David L. On the criteria to be used in decomposing systems into modules. Communications of the ACM. 1972;15(12):1053–1058 December.

[Pat85]Patterson David A. 精简指令集计算机。《ACM 通讯》。1985;28(1):1 月 8-21 日。

[Pat85] Patterson David A. Reduced instruction set computers. Communications of the ACM. 1985;28(1):8–21 January.

[PD80]Patterson David A.,Ditzel David R. 精简指令集计算机的案例。ACM SIGARCH 计算机架构新闻。1980;8(6):10 月 25-33 日。

[PD80] Patterson David A., Ditzel David R. The case for the reduced instruction set computer. ACM SIGARCH Computer Architecture News. 1980;8(6):25–33 October.

[PD12]Peterson Larry L.、Davie Bruce S.计算机网络:系统方法。第五版旧金山,加利福尼亚州:Morgan Kaufmann;2012 年。

[PD12] Peterson Larry L., Davie Bruce S. Computer Networks: A Systems Approach. fifth edition San Francisco, CA: Morgan Kaufmann; 2012.

[宠物81]Peterson Gary L. 关于互斥问题的误解。信息处理快报。1981;12(3):6 月 115-116 日。

[Pet81] Peterson Gary L. Myths about the mutual exclusion problem. Information Processing Letters. 1981;12(3):115–116 June.

[Pey87]Peyton Jones Simon L.函数式编程语言的实现。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1987 年。

[Pey87] Peyton Jones Simon L. The Implementation of Functional ProgrammingLanguages. Englewood Cliffs, NJ: Prentice-Hall; 1987.

[Pey92]Peyton Jones Simon L. 在普通硬件上实现惰性函数式语言:Spineless Tagless G-machine。《函数式编程杂志》。1992;2(2):127–202。

[Pey92] Peyton Jones Simon L. Implementing lazy functional languages on stock hardware: The Spineless Tagless G-machine. Journal of Functional Programming. 1992;2(2):127–202.

[Pey01]Peyton Jones Simon。《解决尴尬小队:Haskell 中的单子输入/输出、并发、异常和外语调用》。收录于:Hoare Tony、Broy Manfred、Steinbruggen Ralf 编。《软件构建的工程理论》。IOS Press;2001:47-96。最初在2000 年Marktoberdorf 暑期学校发表。修订和更正版本可从research.microsoft.com/~simonpj/papers/marktoberdorf/mark.pdf获取。

[Pey01] Peyton Jones Simon. Tackling the Awkward Squad: Monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. In: Hoare Tony, Broy Manfred, Steinbruggen Ralf, eds. Engineering Theories of Software Construction. IOS Press; 2001:47–96. Originally presented at the Marktoberdorf Summer School, 2000. Revised and corrected version available as research.microsoft.com/~simonpj/papers/marktoberdorf/mark.pdf.

[PH12]Patterson David A.、Hennessy John L.计算机组织和设计:硬件-软件接口。第四版,修订版旧金山,加利福尼亚州:Morgan Kaufmann;2012 年。

[PH12] Patterson David A., Hennessy John L. Computer Organization and Design: The Hardware-Software Interface. fourth, revised edition San Francisco, CA: Morgan Kaufmann; 2012.

[馅饼02]Pierce Benjamin C.类型和编程语言。马萨诸塞州剑桥:麻省理工学院出版社;2002 年。

[Pie02] Pierce Benjamin C. Types and Programming Languages. Cambridge, MA: MIT Press; 2002.

[PJ10]Haskell 2010 语言报告。2010年 4 月可在haskell.org/onlinereport/haskell2010/上查阅。

[PJ10] Haskell 2010 Language Report. 2010. April Available at haskell.org/onlinereport/haskell2010/.

[PQ95]Parr Terrence J.,Quong RW ANTLR:一种谓词型LL ( k ) 解析器生成器。软件——实践与经验。1995;25(7):789-810 七月。

[PQ95] Parr Terrence J., Quong R.W. ANTLR: A predicated-LL(k) parser generator. Software—Practice and Experience. 1995;25(7):789–810 July.

[哈巴狗00]Pugh William。Java 内存模型存在致命缺陷。并发性——实践与经验。2000年 5 月 12(6):445-455。

[Pug00] Pugh William. The Java memory model is fatally flawed. Concurrency—Practice and Experience. 2000;12(6):445–455 May.

[Rad82]Radin George。801小型计算机。引自:第一届编程语言和操作系统架构支持国际研讨会论文集;1982 年 3 月,加利福尼亚州帕洛阿尔托,第 39-47 页。

[Rad82] Radin George. The 801 minicomputer. In: Proceedings of the First International Symposium on Architectural Support for Programming Languages and Operating Systems; 1982:39–47 Palo Alto, CA, March.

[84 次回复]Thomas 代表作。《生成基于语言的环境》。马萨诸塞州剑桥:麻省理工学院出版社;1984 年,荣获 1983 年 ACM 博士论文奖。

[Rep84] Reps Thomas. Generating Language-Based Environments. Cambridge, MA: MIT Press; 1984 Winner of the 1983 ACM Doctoral Dissertation Award.

[RF93]Ramakrishna Rau B.,Fisher Joseph A。指令级并行处理:历史、概述和观点。《超级计算杂志》。1993;7(1/2):5 月 9-50 日。

[RF93] Ramakrishna Rau B., Fisher Joseph A. Instruction-level parallel processing: History, overview, and perspective. Journal of Supercomputing. 1993;7(1/2):9–50 May.

[Rob65]Robinson John Alan。基于归结原理的面向机器的逻辑。ACM杂志。1965;12(1):1 月 23-41 日。

[Rob65] Robinson John Alan. A machine-oriented logic based on the resolution principle. Journal of the ACM. 1965;12(1):23–41 January.

[Rob83]Robinson John Alan。逻辑编程——过去、现在和未来。新一代计算。1983;1(2):107–124。

[Rob83] Robinson John Alan. Logic programming—Past, present, and future. New Generation Computing. 1983;1(2):107–124.

[RR64]Randell Brian、Russell Lawford J. 编辑。《ALGOL 60 实现:计算机上 ALGOL 60 程序的翻译和使用》。纽约:Academic Press;1964 APIC 数据处理研究 #5。

[RR64] Randell Brian, Russell Lawford J., eds. ALGOL 60 Implementation: The Translation and Use of ALGOL 60 Programs on a Computer. New York, NY: Academic Press; 1964 A.P.I.C. Studies in Data Processing #5.

[RS59]Rabin Michael O.,Scott Dana S. 有限自动机及其决策问题。IBM研究与开发杂志。1959;3(2):114–125。

[RS59] Rabin Michael O., Scott Dana S. Finite automata and their decision problems. IBM Journal of Research and Development. 1959;3(2):114–125.

[RS70]Rosenkrantz Daniel J.,Stearns Richard E. 确定性自上而下语法的属性。信息与控制。1970;17(3):226-256 十月。

[RS70] Rosenkrantz Daniel J., Stearns Richard E. Properties of deterministic top-down grammars. Information and Control. 1970;17(3):226–256 October.

[RT88]Reps Thomas、Teitelbaum Timothy。合成器生成器:一种基于语言的编辑器构建系统。纽约:Springer-Verlag;1988 年。

[RT88] Reps Thomas, Teitelbaum Timothy. The Synthesizer Generator: A System for Constructing Language-Based Editors. New York, NY: Springer-Verlag; 1988.

[Rub87]Rubin Frank。“GOTO 被认为有害”被认为有害。ACM通讯。1987;30(3):195-196 年 3 月 进一步的通信见第 30 卷第 6、7、8、11 和 12 期。

[Rub87] Rubin Frank. ‘GOTO considered harmful’ considered harmful. Communications of the ACM. 1987;30(3):195–196 March Further correspondence appears in Volume 30, Numbers 6, 7, 8, 11, and 12.

[Rut67]鲁蒂豪瑟·海因茨。ALGOL 60 的描述。纽约,纽约:Springer-Verlag; 1967 年。

[Rut67] Rutishauser Heinz. Description of ALGOL 60. New York, NY: Springer-Verlag; 1967.

[RW92]Reiser Martin、Wirth Niklaus。《Oberon 编程——超越 Pascal 和 Modula》。马萨诸塞州雷丁:Addison-Wesley;1992 年。

[RW92] Reiser Martin, Wirth Niklaus. Programming in Oberon—Steps Beyond Pascal and Modula. Reading, MA: Addison-Wesley; 1992.

[SBG + 91]Strom Robert E.、Bacon David F.、Goldberg Arthur P.、Lowry Andy、Yellin Daniel M. 和 Yem Shaula Alexander 合著。Hermes :一种分布式计算语言。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1991 年。

[SBG+91] Strom Robert E., Bacon David F., Goldberg Arthur P., Lowry Andy, Yellin Daniel M., Yem Shaula Alexander, ini. Hermes: A Language for Distributed Computing. Englewood Cliffs, NJ: Prentice-Hall; 1991.

[SBN82]Siewiorek Daniel P.、Gordon Bell C.、Newell Allen。计算机结构:原理和示例。纽约:McGraw-Hill;1982 年。

[SBN82] Siewiorek Daniel P., Gordon Bell C., Newell Allen. Computer Structures: Principles and Examples. New York, NY: McGraw-Hill; 1982.

[SCG + 13]Shinn Alex、Cowan John、Gleckler Arthur A.、Ganz Steven、Hsu Aaron W.、Lucier Bradley、Medernach Emmanuel、Radul Alexey、Read Jeffrey T.、Rush David、Russel Benjamin L.、Shivers Olin、Snell-Pym Alaric、Sussman Gerald J.、Kelsey Richart、Clinger William、Rees Jonathan、Sperber Michael、Kent Dybvig R.、Flatt Matthew、Stratten Anton van。《算法语言方案报告》第 7 次修订版。2013 年 7 月,由 Shinn、Cowan 和 Gleckler 编辑。可在trac.sacrideo.us/wg/wiki/R7RSHomePage/上找到。

[SCG+13] Shinn Alex, Cowan John, Gleckler Arthur A., Ganz Steven, Hsu Aaron W., Lucier Bradley, Medernach Emmanuel, Radul Alexey, Read Jeffrey T., Rush David, Russel Benjamin L., Shivers Olin, Snell-Pym Alaric, Sussman Gerald J., Kelsey Richart, Clinger William, Rees Jonathan, Sperber Michael, Kent Dybvig R., Flatt Matthew, Stratten Anton van. Revised7 Report on the Algorithmic Language Scheme. 2013. July Edited by Shinn, Cowan, and Gleckler. Available at trac.sacrideo.us/wg/wiki/R7RSHomePage/.

[Sch03]Scholz Sven-Bodo。单一赋值 C——在函数式设置中高效支持高级数组操作。《函数式编程杂志》。2003;13(6):1005–1059。

[Sch03] Scholz Sven-Bodo. Single assignment C—Efficient support for high-level array operations in a functional setting. Journal of Functional Programming. 2003;13(6):1005–1059.

[SCK + 93]Sites Richard L., Chernoff Anton, Kirk Matthew B., Marks Maurice P., Robinson Scott G. 二进制翻译。ACM通讯。1993;36(2):2 月 69-81 日。

[SCK+93] Sites Richard L., Chernoff Anton, Kirk Matthew B., Marks Maurice P., Robinson Scott G. Binary translation. Communications of the ACM. 1993;36(2):69–81 February.

[Sco91]Scott Michael L. Lynx 分布式编程语言:动机、设计和体验。计算机语言。1991;16(3/4):209–233。

[Sco91] Scott Michael L. The Lynx distributed programming language: Motivation, design, and experience. Computer Languages. 1991;16(3/4):209–233.

[Sco13]Scott Michael L.共享内存同步。Morgan & Claypool 出版社;2013 年 6 月计算机架构综合讲座。

[Sco13] Scott Michael L. Shared-Memory Synchronization. Morgan & Claypool Publishers; 2013 Synthesis Lectures on Computer Architecture June.

[SDB84]Schwartz Mayer D.、Delisle Norman M.、Begwani Vimal S. Magpie 中的增量编译。摘自:SIGPLAN '84 编译器构建研讨会论文集;1984:122–131 加拿大魁北克省蒙特利尔,六月。

[SDB84] Schwartz Mayer D., Delisle Norman M., Begwani Vimal S. Incremental compilation in Magpie. In: Proceedings of the SIGPLAN '84 Symposium on Compiler Construction; 1984:122–131 Montreal, Quebec, Canada, June.

[SDDS86]Schwartz Jacob T.、Dewar Robert BK、Dubinsky Ed、Schonberg Edmond。《集合编程:SETL 简介》。纽约:Springer-Verlag;1986 年《计算机科学文本和专著》。

[SDDS86] Schwartz Jacob T., Dewar Robert B.K., Dubinsky Ed, Schonberg Edmond. Programming with Sets: An Introduction to SETL. New York, NY: Springer-Verlag; 1986 Texts and Monographs in Computer Science.

[自卫队+ 07]Sperber Michael、Kent Dybvig R.、Matthew Flatt、Straaten Anton van、Kelsey Richard、Clinger William、Rees Jonathan、Finder Robert Bruce 和 Matthews Jacob。《算法语言方案报告》第 6 版修订版。2007年 9 月由 Sperber、Dybvig、Flatt 和 van Stratten 编辑。可在r6rs.org/上找到。

[SDF+07] Sperber Michael, Kent Dybvig R., Flatt Matthew, Straaten Anton van, Kelsey Richard, Clinger William, Rees Jonathan, Findler Robert Bruce, Matthews Jacob. Revised6 Report on the Algorithmic Language Scheme. 2007. September Edited by Sperber, Dybvig, Flatt, and van Stratten. Available at r6rs.org/.

[SE94]Srivastava Amitabh,Eustace Alan。ATOM :一种用于构建自定义程序分析工具的系统。收录于:SIGPLAN 1994 年编程语言设计和实现会议论文集;1994:196-205 佛罗里达州奥兰多,六月。

[SE94] Srivastava Amitabh, Eustace Alan. ATOM: A system for building customized program analysis tools. In: Proceedings of the SIGPLAN 1994 Conference on Programming Language Design and Implementation; 1994:196–205 Orlando, FL, June.

[Seb15]Sebesta Robert W. 《编程语言概念》。第 11 版,马萨诸塞州波士顿:Pearson/Addison-Wesley;2015 年。

[Seb15] Sebesta Robert W. Concepts of Programming Languages. eleventh edition Boston, MA: Pearson/Addison-Wesley; 2015.

[Sei05]Peter Seibel。实用 Common Lisp。Apress LP;2005 年。

[Sei05] Seibel Peter. Practical Common Lisp. Apress L. P.; 2005.

[第96集]Sethi Ravi。《编程语言:概念和构造》。第二版,马萨诸塞州雷丁:Addison-Wesley;1996 年。

[Set96] Sethi Ravi. Programming Languages: Concepts and Constructs. second edition Reading, MA: Addison-Wesley; 1996.

[SF80]Solomon Marvin H.,Finkel Raphael A. 关于枚举二叉树的注释。《ACM 杂志》。1980;27(1):1 月 3-5 日。

[SF80] Solomon Marvin H., Finkel Raphael A. A note on enumerating binary trees. Journal of the ACM. 1980;27(1):3–5 January.

[SF88]Scott Michael L.,Finkel Raphael A. 一种跨编译单元类型安全的简单机制。IEEE软件工程学报。1988;SE–14(8):8 月 1238–1239 日。

[SF88] Scott Michael L., Finkel Raphael A. A simple mechanism for type security across compilation units. IEEE Transactions on Software Engineering. 1988;SE–14(8):1238–1239 August.

[西弗吉尼亚足球超级联赛+ 94]Schoinas Ioannis、Falsafi Babak、Lebeck Alvin R.、Reinhardt Steven K.、Larus James R.、Wood David A.分布式共享内存的细粒度访问控制。《第六届编程语言和操作系统架构支持国际会议论文集》;1994 年 10 月,加利福尼亚州圣何塞,297-306。

[SFL+94] Schoinas Ioannis, Falsafi Babak, Lebeck Alvin R., Reinhardt Steven K., Larus James R., Wood David A. Fine-grain access control for distributed shared memory. In: Proceedings of the Sixth International Conference on Architectural Support for Programming Languages and Operating Systems; 1994:297–306 San Jose, CA, October.

[第 96 号国家标准]Scales Daniel J.、Gharachorloo Kourosh。Shasta :一种支持细粒度共享内存的低开销纯软件方法。收录于:第七届编程语言和操作系统架构支持国际会议论文集;1996:174–185 马萨诸塞州剑桥,十月。

[SG96] Scales Daniel J., Gharachorloo Kourosh. Shasta: A low overhead, software-only approach for supporting fine-grain shared memory. In: Proceedings of the Seventh International Conference on Architectural Support for Programming Languages and Operating Systems; 1996:174–185 Cambridge, MA, October.

[SGC13]Syme Don、Granicz Adam、Cisternino Antonio。Expert F# 3.0。加州伯克利:Apress;2013 年。

[SGC13] Syme Don, Granicz Adam, Cisternino Antonio. Expert F# 3.0. Berkeley, CA: Apress; 2013.

[SH92]Sajeev ASM,John Hurst A. χ中的编程持久性。IEEE计算机。1992;25(9):9 月 57-66 日。

[SH92] Sajeev A.S.M., John Hurst A. Programming persistence in χ. IEEE Computer. 1992;25(9):57–66 September.

[SHC96]Somogyi Zoltan、Henderson Fergus、Conway Thomas。《Mercury 的执行算法:一种高效的纯声明式逻辑编程语言》。《逻辑编程杂志》。1996;29(1-3):10 月至 12 月 17-64 日。

[SHC96] Somogyi Zoltan, Henderson Fergus, Conway Thomas. The execution algorithm of Mercury: An efficient purely declarative logic programming language. Journal of Logic Programming. 1996;29(1-3):17–64 October–December.

[Sie00]Siegel Jon。CORBA 3 基础与编程。纽约:John Wiley and Sons;2000 年。

[Sie00] Siegel Jon. CORBA 3 Fundamentals and Programming. New York, NY: John Wiley and Sons; 2000.

[Sip13]Sipser Michael。《计算理论简介》。第三版,马萨诸塞州波士顿:Cengage Learning;2013 年。

[Sip13] Sipser Michael. Introduction to the Theory of Computation. third edition Boston, MA: Cengage Learning; 2013.

[SIT72]站点 Richard L. Algol W 参考手册。斯坦福,加利福尼亚州:斯坦福大学计算机科学系;1972 年技术报告 STAN-CS-71-230 二月。

[Sit72] Sites Richard L. Algol W reference manual. Stanford, CA: Computer Science Department, Stanford University; 1972 Technical Report STAN-CS-71-230 February.

[SK95]Slonneger Kenneth,Kurtz Barry L.编程语言的形式语法和语义:基于实验室的方法。马萨诸塞州雷丁:Addison-Wesley;1995 年。

[SK95] Slonneger Kenneth, Kurtz Barry L. Formal Syntax and Semantics of Programming Languages: A Laboratory Based Approach. Reading, MA: Addison-Wesley; 1995.

[SMC91]Saltz Joel H.、Mirchandaney Avi、Crowley Kay。循环的运行时并行化和调度。IEEE计算机学报。1991;40(5):5 月 603-612 日。

[SMC91] Saltz Joel H., Mirchandaney Avi, Crowley Kay. Run-time parallelization and scheduling of loops. IEEE Transactions on Computers. 1991;40(5):603–612 May.

[SPSS08]Shirako Jun、Peixotto David、Sarkar Vivek、Scherer William N. III。Phasers :用于集体和点对点同步的统一无死锁构造。收录于:第二十二届国际超级计算会议论文集;2008 年:277–288 希腊科斯岛,六月。

[SPSS08] Shirako Jun, Peixotto David, Sarkar Vivek, Scherer William N. III. Phasers: A unified deadlock-free construct for collective and point-to-point synchronization. In: Proceedings of the Twenty-Second International Conference on Supercomputing; 2008:277–288 Island of Kos, Greece, June.

[SR13]Sahami Mehran、Roach Steve 编。《计算机科学课程 2013:计算机科学本科学位课程指南》。2013年。计算机课程联合工作组、计算机协会 (ACM) 和 IEEE 计算机学会,12 月,可访问acm.org/education/CS2013-final-report.pdf

[SR13] Sahami Mehran, Roach Steve, eds. Computer Science Curricula 2013: Curriculum Guidelines for Undergraduate Degree Programs in Computer Science. 2013. Joint Task Force on Computing Curricula, Association for Computing Machinery (ACM) and the IEEE Computer Society, December Available as acm.org/education/CS2013-final-report.pdf.

[斯里兰卡95]Srinivasan Raj。RPC :远程过程调用协议规范第 2 版。1995 年。互联网征求意见稿 #1831,8 月,可在rfc-archive.org/getrfc.php?rfc=1831上找到。

[Sri95] Srinivasan Raj. RPC: Remote procedure call protocol specification version 2. 1995. Internet Request for Comments #1831, August Available at rfc-archive.org/getrfc.php?rfc=1831.

[SS71]Scott Dana S.、Strachey Christopher。《面向计算机语言的数学语义》。收录于:Fox Jerome 编。《计算机和自动机研讨会论文集》。纽约:布鲁克林理工学院出版社;1971 年:19-46 页。

[SS71] Scott Dana S., Strachey Christopher. Toward a mathematical semantics for computer language. In: Fox Jerome, ed. Proceedings, Symposium on Computers and Automata. New York, NY: Polytechnic Institute of Brooklyn Press; 1971:19–46.

[SSA13]Schkufza Eric、Sharma Rahul、Aiken Alex。随机超优化。刊于:第十八届编程语言和操作系统架构支持国际会议论文集;2013 年:305–316 德克萨斯州休斯顿,3 月。

[SSA13] Schkufza Eric, Sharma Rahul, Aiken Alex. Stochastic superoptimization. In: Proceedings of the Eighteenth International Conference on Architectural Support for Programming Languages and Operating Systems; 2013:305–316 Houston, TX, March.

[SSD13]Sutton Andrew、Stroustrup Bjarne、Reis Gabriel Dos。Concepts Lite:使用谓词约束模板。2013年 3 月,国际标准化组织概念工作组,文档编号 N3580。可从open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3580.pdf获取。

[SSD13] Sutton Andrew, Stroustrup Bjarne, Reis Gabriel Dos. Concepts Lite: Constraining Templates with Predicates. 2013. March Document number N3580, Concepts working group, International Organization for Standardization. Available as open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3580.pdf.

[Sta95]Stansifer Ryan D. 《编程语言研究》。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1995 年。

[Sta95] Stansifer Ryan D. The Study of Programming Languages. Englewood Cliffs, NJ: Prentice-Hall; 1995.

[Ste90]Jr Guy L. Steele。Common Lisp—语言。第二版,马萨诸塞州贝德福德:Digital Press;1990 年。可从cs.cmu.edu/Groups/AI/html/cltl/cltl2.html获取。

[Ste90] Jr Guy L. Steele. Common Lisp—The Language. second edition Bedford, MA: Digital Press; 1990. Available at cs.cmu.edu/Groups/AI/html/cltl/cltl2.html.

[Sto77]Stoy Joseph E.指称语义:Scott-Strachey 编程语言语义学方法,第 1 卷。马萨诸塞州剑桥:麻省理工学院出版社;1977 年。

[Sto77] Stoy Joseph E. Denotational Semantics: The Scott-Strachey Approach to Programming Language Semantics, volume 1. Cambridge, MA: MIT Press; 1977.

[第13话]Stroustrup Bjarne。《C++编程语言》。第四版 Addison-Wesley Professional;2013 年第二版,1991 年。

[Str13] Stroustrup Bjarne. The C++Programming Language. fourth edition Addison-Wesley Professional; 2013 Second edition, 1991.

[太阳90]Sunderam Vaidyalingam S. PVM:并行分布式计算框架。并发性——实践与经验。1990;2(4):315-339 十二月。

[Sun90] Sunderam Vaidyalingam S. PVM: A framework for parallel distributed computing. Concurrency—Practice and Experience. 1990;2(4):315–339 December.

[Sun97]Sun Microsystems,加利福尼亚州山景城。JavaBeans。19978 月,版本 1.01-A。可从oracle.com/technetwork/articles/javaee/spec-136004.html获取。

[Sun97] Sun Microsystems, Mountain View, CA. JavaBeans. 1997. August Version 1.01-A. Available at oracle.com/technetwork/articles/javaee/spec-136004.html.

[太阳04]Sundell H.高效实用的非阻塞数据结构。瑞典哥德堡:查尔姆斯理工大学和哥德堡大学;2004 年。计算机科学系博士论文,网址为cs.chalmers.se/~tsigas/papers/Haakan-Thesis.pdf

[Sun04] Sundell H. Efficient and Practical Non-Blocking Data Structures. Goteborg, Sweden: Chalmers University of Technology and Goteborg University; 2004. Ph. D. dissertation, Department of Computing Science Available as cs.chalmers.se/~tsigas/papers/Haakan-Thesis.pdf.

[星期日06]Sun Microsystems。Strongtalk :需要速度的 Smalltalk。2006。strongtalk.org

[Sun06] Sun Microsystems. Strongtalk: Smalltalk with a need for speed. 2006. strongtalk.org.

[SW67]Schorr Herbert,Waite William M. 一种高效的独立于机器的各种列表结构垃圾收集程序。ACM通讯。1967;10(8):8 月 501-506 日。

[SW67] Schorr Herbert, Waite William M. An efficient machine-independent procedure for garbage collection in various list structures. Communications of the ACM. 1967;10(8):501–506 August.

[SW94]Smith James E.、Weiss Shlomo。PowerPC 601 和 Alpha 21064:两个 RISC 的故事。IEEE计算机。1994;27(6):6 月 46-58 日。

[SW94] Smith James E., Weiss Shlomo. PowerPC 601 and Alpha 21064: A tale of two RISCs. IEEE Computer. 1994;27(6):46–58 June.

[SZBH86]Swinehart Daniel C.、Zellweger Polle T.、Beach Richard J.、Hagmann Robert B. 的结构视图Cedar 编程环境。ACM编程语言和系统事务。1986;8(4):10 月 419-490。

[SZBH86] Swinehart Daniel C., Zellweger Polle T., Beach Richard J., Hagmann Robert B. A structural view of the Cedar programming environment. ACM Transactions on Programming Languages and Systems. 1986;8(4):419–490 October.

[Tan78]Tanenbaum Andrew S. Pascal 与 ALGOL 68 的比较。计算机杂志。1978;21(4):316-323 十一月。

[Tan78] Tanenbaum Andrew S. A comparison of Pascal and ALGOL 68. The Computer Journal. 1978;21(4):316–323 November.

[TFH13]Thomas Dave、Fowler Chad、Hunt Andy。《Ruby 1.9 和 2.0 编程——实用程序员指南》。LLC,德克萨斯州达拉斯:实用程序员;2013 年。

[TFH13] Thomas Dave, Fowler Chad, Hunt Andy. Programming Ruby 1.9 & 2.0—The Pragmatic Programmers' Guide. LLC, Dallas, TX: The Pragmatic Programmers; 2013.

[Tho95]Thompson Tom。构建更好的虚拟 CPU。字节。1995;20(8):149-150 八月。

[Tho95] Thompson Tom. Building the better virtual CPU. Byte. 1995;20(8):149–150 August.

[Tic86]Tichy Walter F. 智能重新编译。ACM编程语言和系统事务。1986;8(3):7 月 273-291 日。

[Tic86] Tichy Walter F. Smart recompilation. ACM Transactions on Programming Languages and Systems. 1986;8(3):273–291 July.

[TM81]Teitelman Warren,Masinter Larry。Interlisp 编程环境。IEEE计算机。1981;14(4):4 月 25-33 日。

[TM81] Teitelman Warren, Masinter Larry. The Interlisp programming environment. IEEE Computer. 1981;14(4):25–33 April.

[TML13]Kevin Tatroe、Peter MacIntyre、Lerdorf Rasmus。《PHP 编程》。第三版 Sebastopol,CA:O'Reilly Media;2013 年。

[TML13] Tatroe Kevin, MacIntyre Peter, Lerdorf Rasmus. Programming PHP. third edition Sebastopol, CA: O'Reilly Media; 2013.

[TR81]Teitelbaum Timothy,Reps Thomas。康奈尔程序合成器:语法制导编程环境。ACM通讯。1981;24(9):563-573 九月。

[TR81] Teitelbaum Timothy, Reps Thomas. The Cornell Program Synthesizer: A syntax-directed programming environment. Communications of the ACM. 1981;24(9):563–573 September.

[Tre86]Kent Treiber R.系统编程:应对并行性。IBM Almaden 研究中心;1986 年技术报告 RJ 5118 四月。

[Tre86] Kent Treiber R. Systems programming: Coping with parallelism. IBM Almaden Research Center; 1986 Technical Report RJ 5118 April.

[Tur86]Turner David A. Miranda 概述。ACM SIGPLAN 通知。1986;21(12):158-166 十二月。

[Tur86] Turner David A. An overview of Miranda. ACM SIGPLAN Notices. 1986;21(12):158–166 December.

[TW12]Tanenbaum Andrew S.、Wetherall David J.计算机网络。第五版 Pearson Higher Education;2012 年。

[TW12] Tanenbaum Andrew S., Wetherall David J. Computer Networks. fifth edition Pearson Higher Education; 2012.

[TWGM07]Tabba Fuad、Wang Cong、Goodman James R.、Moir Mark。NZTM :非阻塞零间接事务内存。见:第二届 ACM SIGPLAN 事务计算研讨会,俄勒冈州波特兰;2007 年 8 月,网址为cs.rochester.edu/meetings/TRANSACT07/papers/tabba.pdf

[TWGM07] Tabba Fuad, Wang Cong, Goodman James R., Moir Mark. NZTM: Nonblocking zero-indirection transactional memory. In: Second ACM SIGPLAN Workshop on Transactional Computing, Portland, OR; 2007. August Available as cs.rochester.edu/meetings/TRANSACT07/papers/tabba.pdf.

[Ull85]Ullman Jeffrey D. 数据库逻辑查询语言的实现。ACM数据库系统事务。1985;10(3):9 月 289-321 日。

[Ull85] Ullman Jeffrey D. Implementation of logical query languages for databases. ACM Transactions on Database Systems. 1985;10(3):289–321 September.

[美国91]Ungar David,Smith Randall B. SELF:简单的力量。Lisp和符号计算。1991;4(3):7 月 187-205。

[US91] Ungar David, Smith Randall B. SELF: The power of simplicity. Lisp and Symbolic Computation. 1991;4(3):187–205 July.

[UW08]Ullman Jeffrey D.、Widom Jennifer。《数据库系统入门课程》。第三版 Upper Saddle River,新泽西州:Pearson/Prentice-Hall;2008 年。

[UW08] Ullman Jeffrey D., Widom Jennifer. A First Course in Database Systems. third edition Upper Saddle River, NJ: Pearson/Prentice-Hall; 2008.

[vCZ + 03]Behren Rob von、Condit Jeremy、Zhou Feng、Necula George C.、Brewer Eric。Capriccio :互联网服务的可扩展线程。收录于:第十九届 ACM 操作系统原理研讨会论文集;2003 年:268–281 博尔顿码头(乔治湖),纽约州,十月。

[vCZ+03] Behren Rob von, Condit Jeremy, Zhou Feng, Necula George C., Brewer Eric. Capriccio: Scalable threads for internet services. In: Proceedings of the Nineteenth ACM Symposium on Operating Systems Principles; 2003:268–281 Bolton Landing (Lake George), NY, October.

[VF82]Virgilio Thomas R.,Finkel Raphael A. 绑定策略和范围规则是独立的。计算机语言。1982;7(2):61-67。

[VF82] Virgilio Thomas R., Finkel Raphael A. Binding strategies and scope rules are independent. Computer Languages. 1982;7(2):61–67.

[VF94]Veenstra Jack E.、Fowler Robert J. Mint:共享内存多处理器高效仿真的前端。收录于:第二届计算机和电信系统建模、分析和仿真国际研讨会论文集;1994:201–207 北卡罗来纳州达勒姆,1 月。

[VF94] Veenstra Jack E., Fowler Robert J. Mint: A front end for efficient simulation of shared-memory multiprocessors. In: Proceedings of the Second International Workshop on Modeling, Analysis and Simulation of Computer and Telecommunication Systems; 1994:201–207 Durham, NC, January.

[虚拟点数+ 75]van Wijngaarden A.、Mailloux BJ、Peck JE.L.、Koster CHA、Sintzoff M.、Lindsey CH、Meertens LGLT.、Fisker RG《算法语言 ALGOL 68 的修订报告》。《Acta Informatica》。1975;5(1–3):1–236 另见 ACM SIGPLAN Notices,12(5):1–70,1977 年 5 月。

[vMP+75] van Wijngaarden A., Mailloux B.J., Peck JE.L., Koster C.H.A., Sintzoff M., Lindsey C.H., Meertens L.G.LT., Fisker R.G. Revised report on the algorithmic language ALGOL 68. Acta Informatica. 1975;5(1–3):1–236 Also ACM SIGPLAN Notices, 12(5):1–70, May 1977.

[vRD11]Guido van Rossum、Fred L.Drake Jr 编。《Python 语言参考手册》(3.2 版)。英国布里斯托尔:Network Theory, Ltd.;2011 年。

[vRD11] Guido van Rossum, Fred L.Drake Jr, eds. The Python Language Reference Manual (version 3.2). Bristol, UK: Network Theory, Ltd.; 2011.

[Wad98a]Wadler Philip。《六个愤怒的人》。ACM SIGPLAN 通告。1998;33(2):2 月 25-30 日。注意:本期封面上的目录不正确。

[Wad98a] Wadler Philip. An angry half-dozen. ACM SIGPLAN Notices. 1998;33(2):25–30 February NB: table of contents on cover of issue is incorrect.

[Wad98b]Wadler Philip。为什么没有人使用函数式语言。ACM SIGPLAN 通知。1998;33(8):8 月 23-27 日。

[Wad98b] Wadler Philip. Why no one uses functional languages. ACM SIGPLAN Notices. 1998;33(8):23–27 August.

[Wat77]Watt David Anthony。词缀语法的解析问题。Acta Informatica。1977;8(1):1-20。

[Wat77] Watt David Anthony. The parsing problem for affix grammars. Acta Informatica. 1977;8(1):1–20.

[Web89]Webb Fred。Fortran故事 — 真正的独家新闻。提交给 alt.folklore.computers。1989年 Mark Brader 在 ACM RISKS在线论坛第 9 卷第 54 期(1989 年 12 月 12 日)中引用。

[Web89] Webb Fred. Fortran story—The real scoop. Submitted to alt.folklore.computers. 1989 Quoted by Mark Brader in the ACM RISKS on-line forum, volume 9, issue 54, December 12, 1989.

[Weg90]Peter Wegner。面向对象编程的概念和范例。OOPS Messenger。1990 ;1(1):7-87 八月OOPSLA '89主题演讲的扩展版本。

[Weg90] Wegner Peter. Concepts and paradigms of object-oriented programming. OOPS Messenger. 1990;1(1):7–87 August Expanded version of the keynote address from OOPSLA '89.

[湿78]Wettstein Horst。重新审视嵌套监控调用问题。ACM操作系统评论。1978;12(1):1 月 19-23 日。

[Wet78] Wettstein Horst. The problem of nested monitor calls revisited. ACM Operating Systems Review. 1978;12(1):19–23 January.

[Wex78]Wexelblat Richard L. 编辑,ACM SIGPLAN 编程语言历史 (HOPL) 会议论文集,ACM 专著系列,1981 年,加利福尼亚州洛杉矶;纽约州纽约:Academic Press;1978 年 6 月。

[Wex78] Wexelblat Richard L., ed. Proceedings of the ACM SIGPLAN History of Programming Languages (HOPL) Conference, ACM Monograph Series, 1981, Los Angeles, CA; New York, NY: Academic Press; 1978 June.

[WH66]Wirth Niklaus,Hoare Charles Antony Richard。对 ALGOL 开发的贡献。ACM通讯。1966;9(6):6 月 413-431。

[WH66] Wirth Niklaus, Hoare Charles Antony Richard. A contribution to the development of ALGOL. Communications of the ACM. 1966;9(6):413–431 June.

[Wil92a]Wilson Paul R.页面错误时的指针调换:在标准硬件上高效且兼容地支持巨大地址空间。摘自:《操作系统面向对象国际研讨会论文集》;1992:364–377,法国巴黎,9 月。

[Wil92a] Wilson Paul R. Pointer swizzling at page fault time: Efficiently and compatibly supporting huge address spaces on standard hardware. In: Proceedings of the International Workshop on Object Orientation in Operating Systems; 1992:364–377 Paris, France, September.

[Wil92b]Wilson Paul R.单处理器垃圾收集技术。引自:《国际内存管理研讨会论文集》 , 《计算机科学讲义》第 637 卷;德国柏林:Springer-Verlag;1992 年:1-42。研讨会于 1992 年 9 月在法国圣马洛举行。扩展版本可从ftp://ftp.cs.utexas.edu/pub/garbage/bigsurv.ps获取。

[Wil92b] Wilson Paul R. Uniprocessor garbage collection techniques. In: Proceedings of the International Workshop on Memory Management, volume 637 of Lecture Notes in Computer Science; Berlin, Germany: Springer-Verlag; 1992:1–42. Workshop held at St. Malo, France, September 1992. Expanded version available as ftp://ftp.cs.utexas.edu/pub/garbage/bigsurv.ps.

[Win93]Winskel Glynn。《编程语言的形式语义》。马萨诸塞州剑桥:麻省理工学院出版社;1993 年。

[Win93] Winskel Glynn. The Formal Semantics of Programming Languages. Cambridge, MA: MIT Press; 1993.

[Wir71]Wirth Niklaus。编程语言 Pascal。Acta Informatica。1971;1(1):35–63。

[Wir71] Wirth Niklaus. The programming language Pascal. Acta Informatica. 1971;1(1):35–63.

[Wir76]Wirth Niklaus。算法 + 数据结构 = 程序。新泽西州恩格尔伍德克利夫斯:Prentice-Hall;1976 年。

[Wir76] Wirth Niklaus. Algorithms + Data Structures = Programs. Englewood Cliffs, NJ: Prentice-Hall; 1976.

[Wir77a]Wirth Niklaus。Modula 的设计和实现。软件——实践与经验。1977;7(1):67–84 一月至二月。

[Wir77a] Wirth Niklaus. Design and implementation of Modula. Software—Practice and Experience. 1977;7(1):67–84 January–February.

[Wir77b]Wirth Niklaus。Modula:一种模块化多道程序设计语言。软件——实践与经验。1977;7(1):1 月至 2 月 3-35 日。

[Wir77b] Wirth Niklaus. Modula: A language for modular multiprogramming. Software—Practice and Experience. 1977;7(1):3–35 January–February.

[Wir80]Wirth Niklaus。模块:高级编程语言中的系统结构工具。收录于:Tobias Jeffrey M. 主编的《语言设计和编程方法论》 , 《计算机科学讲义》第 79 卷。西德柏林:Springer-Verlag;1980:1-24 1979 年 9 月在澳大利亚悉尼举行的研讨会论文集。

[Wir80] Wirth Niklaus. The module: A system structuring facility in high-level programming languages. In: Tobias Jeffrey M., ed. Language Design and Programming Methodology, volume 79 of Lecture Notes in Computer Science. Berlin, West Germany: Springer-Verlag; 1980:1–24 Proceedings of a symposium held at Sydney, Australia, September 1979.

[Wir85a]Wirth Niklaus。从编程语言设计到计算机构造。ACM通讯。1985;28(2):159–164 二月 1984 年图灵奖演讲。

[Wir85a] Wirth Niklaus. From programming language design to computer construction. Communications of the ACM. 1985;28(2):159–164 February The 1984 Turing Award lecture.

[Wir85b]Wirth Niklaus。《Modula-2 编程》。第三版,修订版纽约:Springer-Verlag;1985 计算机科学文本和专著。

[Wir85b] Wirth Niklaus. Programming in Modula-2. third, corrected edition New York, NY: Springer-Verlag; 1985 Texts and Monographs in Computer Science.

[Wir88a]Wirth Niklaus。从 Modula 到 Oberon。软件——实践与经验。1988;18(7):661–670 七月。

[Wir88a] Wirth Niklaus. From Modula to Oberon. Software—Practice and Experience. 1988;18(7):661–670 July.

[Wir88b]Wirth Niklaus。编程语言 Oberon。软件——实践与经验。1988;18(7):671-690 七月。

[Wir88b] Wirth Niklaus. The programming language Oberon. Software—Practice and Experience. 1988;18(7):671–690 July.

[Wir88c]Wirth Niklaus。类型扩展。ACM编程语言和系统汇刊。1988;10(2):204-214 四月 相关通信见第 13 卷第 4 期。

[Wir88c] Wirth Niklaus. Type extensions. ACM Transactions on Programming Languages and Systems. 1988;10(2):204–214 April Relevant correspondence appears in Volume 13, Number 4.

[Wir07] NiklausWirth。Modula-2 和 Oberon。在HOPL III 会议论文集[Ass07] 中,第 3-1 至 3-10 页。

[Wir07] NiklausWirth. Modula-2 and Oberon. In HOPL III Proceedings [Ass07], pages 3-1–3-10.

[WJ93]Wilson Paul R.,Johnstone Mark S.实时非复制垃圾收集。出处:OOPSLA '93 内存管理和垃圾收集研讨会,华盛顿特区;1993 年 9 月。

[WJ93] Wilson Paul R., Johnstone Mark S. Real-time non-copying garbage collection. In: OOPSLA '93 Workshop on Memory Management and Gargage Collection, Washington, DC; 1993 September.

[WJH03]Welch Brent B.、Jones Ken、Hobbs Jeffrey。《Tcl 和 Tk 实用编程》。第四版 Upper Saddle River,新泽西州:Prentice-Hall;2003 以前版本的样章可在 beedub.com/book/ 在线获取。

[WJH03] Welch Brent B., Jones Ken, Hobbs Jeffrey. Practical Programming in Tcl and Tk. fourth edition Upper Saddle River, NJ: Prentice-Hall; 2003 Sample chapters from previous editions available on-line at beedub.com/book/.

[WLAG93]Wahbe Robert、Lucco Steven、Anderson Thomas E.、Graham Susan L.高效的基于软件的故障隔离。收录于:第十四届 ACM 操作系统原理研讨会论文集;1993:203–216 北卡罗来纳州阿什维尔,十二月。

[WLAG93] Wahbe Robert, Lucco Steven, Anderson Thomas E., Graham Susan L. Efficient software-based fault isolation. In: Proceedings of the Fourteenth ACM Symposium on Operating Systems Principles; 1993:203–216 Asheville, NC, December.

[WM95]Wilhelm Reinhard,Maurer Dieter。《编译器设计》。英国沃金厄姆:Addison-Wesley;1995 年由 Stephen S. Wilson 从德文译出。

[WM95] Wilhelm Reinhard, Maurer Dieter. Compiler Design. Wokingham, England: Addison-Wesley; 1995 Translated from the German by Stephen S. Wilson.

[WMWM87]Walker Janet H.、Moon David A.、Weinreb Daniel L.、McMahon Mike。Symbolics Genera 编程环境。IEEE软件。1987;4(6):11 月 36-45 日。

[WMWM87] Walker Janet H., Moon David A., Weinreb Daniel L., McMahon Mike. The Symbolics Genera programming environment. IEEE Software. 1987;4(6):36–45 November.

[Wol96]Wolfe Michael。《并行计算的高性能编译器》。加利福尼亚州雷德伍德城:Addison-Wesley;1996 年。

[Wol96] Wolfe Michael. High Performance Compilers for Parallel Computing. Redwood City, CA: Addison-Wesley; 1996.

[Wor05]万维网联盟。万维网字符模型 1.0:基础知识。2005年 2 月,网址为w3.org/TR/charmod/

[Wor05] World Wide Web Consortium. Character Model for the World Wide Web 1.0: Fundamentals. 2005. February Available at w3.org/TR/charmod/.

[Wor06a]万维网联盟。可扩展标记语言 (XML) 1.1。第二版 2006 年 9 月 可在w3.org/TR/xml11/上获取。

[Wor06a] World Wide Web Consortium. Extensible Markup Language (XML) 1.1. Second Edition 2006. September Available at w3.org/TR/xml11/.

[Wor06b]万维网联盟。可扩展样式表语言 (XSL) 版本 1.1。2006年 12 月,可在w3.org/TR/xsl/上获取。

[Wor06b] World Wide Web Consortium. Extensible Stylesheet Language (XSL) Version 1.1. 2006. December Available at w3.org/TR/xsl/.

[Wor07]万维网联盟。XML路径语言 (XPath) 2.0。2007年 1 月,可在w3.org/TR/xpath20/上获取。

[Wor07] World Wide Web Consortium. XML Path Language (XPath) 2.0. 2007. January Available at w3.org/TR/xpath20/.

[Wor12]万维网联盟。SOAP当前状态。2012年。w3.org /standards/techs /soap。

[Wor12] World Wide Web Consortium. SOAP current status. 2012. w3.org/standards/techs/soap.

[Wor14]万维网联盟。XSL转换 (XSLT) 版本 3.0。2014年 10 月,可在w3.org/TR/xslt-30/上获取。

[Wor14] World Wide Web Consortium. XSL Transformations (XSLT) Version 3.0. 2014. October Available at w3.org/TR/xslt-30/.

[世界卫生大会第 77 届会议]Welsh Jim、Sneeringer WJ、Hoare Charles Antony Richard。Pascal 中的歧义和不安全性。软件实践与经验。1977;7(6):685–696 十一月至十二月。

[WSH77] Welsh Jim, Sneeringer W.J., Hoare Charles Antony Richard. Ambiguities and insecurities in Pascal. Software—Practice and Experience. 1977;7(6):685–696 November–December.

[YA93]Yang Jae-Heon,Anderson James H.快速、可扩展的同步,只需最少的硬件支持(扩展摘要)。摘自:第十二届 ACM 分布式计算原理研讨会论文集;1993:171–182 伊萨卡,纽约,八月。

[YA93] Yang Jae-Heon, Anderson James H. Fast, scalable synchronization with minimal hardware support (extended abstract). In: Proceedings of the Twelfth Annual ACM Symposium on Principles ofDistributed Computing; 1993:171–182 Ithaca, NY, August.

[你67]Younger Daniel H. 时间n 3中上下文无关语言的识别和解析。信息与控制。1967;10(2):189–208 年 2 月。

[You67] Younger Daniel H. Recognition and parsing of context-free languages in time n3. Information and Control. 1967;10(2):189–208 February.

[YTEM04]Yang Junfeng、Twohey Paul、Engler Dawson、Musuvathi Madanlal。使用模型检查查找严重的文件系统错误。在:第六届 USENIX 操作系统设计和实现研讨会论文集;2004 年:273–288 旧金山,加利福尼亚州,十二月。

[YTEM04] Yang Junfeng, Twohey Paul, Engler Dawson, Musuvathi Madanlal. Using model checking to find serious file system errors. In: Proceedings of the Sixth USENIX Symposium on Operating Systems Design and Implementation; 2004:273–288 San Francisco, CA, December.

[Zho96]周能发。再论Prolog实现中的参数传递和控制堆栈管理。ACM编程语言和系统学报。1996 ;18(6):752–779 十一月。

[Zho96] Zhou Neng-Fa. Parameter passing and control stack management in Prolog implementation revisited. ACM Transactions on Programming Languages and Systems. 1996;18(6):752–779 November.

[ZRA + 08]Zhao Qin、Rabbah Rodric、Amarasinghe Saman、Rudolph Larry、Wong Weng-Fai。如何完成一百万个观察点:使用动态检测进行高效调试。在:第十七届国际编译器构建会议论文集;2008:147-162 匈牙利布达佩斯,3 月。

[ZRA+08] Zhao Qin, Rabbah Rodric, Amarasinghe Saman, Rudolph Larry, Wong Weng-Fai. How to do a million watchpoints: Efficient debugging using dynamic instrumentation. In: Proceedings of the Seventeenth International Conference on Compiler Construction; 2008:147–162 Budapest, Hungary, March.

指数

Index

在每个条目中,首先列出正文中的页面,然后列出配套网站上的页面。

In each entry, pages in the main text are listed first, followed by pages on the companion site.

“ff”标记表明内容将继续涵盖后续页面。

The “ff” designation indicates that coverage continues on following pages.

0-9 和符号

0-9, and symbols

680x0 架构 829
字节序 344

一个

A

A 名单。  参见 协会名单
ABA 问题 690
阿贝尔森,哈罗德(哈尔)  178 , 590 , 771
亚伯拉罕斯,大卫 349
绝对的。  另请参阅 提示
监控信号为 671ff
抽象语法树。  请参阅 语法树、抽象
Java 抽象窗口工具包  c -149
抽象 8 , 15 , 177 , 285 , 295 , 300ff , 349 , 411 另请参阅 控制抽象数据抽象
胁迫 
定义 115 , 221
使用泛型 335
λ  c -214
程序 
随机数生成器作为示例 137
和反思 838
基于抽象的类型视图 300
访问器,在 C  # 459、477
确认消息  c -242ff
动作例程 32 , 180 , 195ff , 200 c -20
自下而上的评价 199
和错误恢复  c -8; c -11ff
和管理属性空间  c -45ff
和语义堆栈  c -53
激活记录。  请参见 堆栈框架
活动服务器页面,微软(ASP)  731
活动 X 530
实际参数。  请参阅 参数、实际
临时多态性 336 另请参阅 重载
临时翻译方案 198
艾达 7 , 12 , 285 , 294 , 859
接受声明  c -236;c -245
访问(指针)类型 378ff , 382ff , 389 , 407
聚合 238 , 304 , 382 ,​​ 538
别名对象 407
数组 321 , 359ff , 362 c -48
动态 365ff
有界缓冲区示例 675 c -245ff
字母大小写 
案例陈述 257 , 286
类范围的类型 508
胁迫 321
通信模型 636
并发性。  另请参阅 Ada、任务
常数 120
悬垂引用 389 , 407
声明语法  c -54
选择语句  中的延迟替代方案c -246
指称语义 214
取消引用,自动 383
阐述 126
条目  c -235
枚举类型 308
错误程序 33 , 427 , 663
异常处理 441 , 468
退出语句 276
指数运算 227
定点类型 305 , 344
浮点常数 106
for循环 325
函数返回值 439
垃圾收集 390
泛型 333ff
制约因素 335
GNU 编译器 ( gnat467 , 782 c -285
goto语句 246
历史 9
输入/输出 433 c -153ff
if语句 256 , 422
内联子程序 419
接口类 509 , 516
不变量(契约)  183
迭代器对象 268ff
局部物体的有限范围  c -191
消息传递 674 c -245
方法绑定 507 , 527
方法调用语法 491
运算符 345
多字节字符 47
嵌套子程序 127
行动 
数字类型 322
纽约大学口译员 214
对象初始化 497ff
运算符函数 148 , 225
超载 147ff , 309
包(模块)  137 , 138 , 486
分层(嵌套)  c -41
打包类型 354 , 357
参数传递 423 , 424 , 427ff , 433ff , 464 , 468 c -250ff
指针。  请参阅 Ada,访问(指针)类型
优先级别 228
私有类型 336
受保护对象 674 c -245
接收操作  c -244
记录 322 , 355
rem运算符 345
远程调用  c -240
会合  c -249
重复循环,缺少 275
决议 范围
范围规则 132 , 134 , 412
选择语句  c -246
另编 165 c -36
短路评估 245 , 256
函数中的副作用 253
字符串 376
子范围类型 310
子程序作为参数 155 , 157 , 431 c -174
亚型 310 , 315
同步 674ff
select语句  中终止替代项c -246;c -255
类型属性 336 , 407 , 434
类型检查 183 , 313 , 315 , 320
类型约束 310
类型转换 317
类型声明,不完整 382
未检查转换 319
未经检查的指令 68 c -160
变量作为值 498
变体记录  c -139ff; c -160ff
类成员的可见性 491
地址空间组织 790ff
地址 
数组元素 371ff
汇编程序796ff分配给名称 
作为数据格式  c -63
和指针 378
段和 791
虚拟和 物理
寻址模式  c -71ff
位移  c -72
循环展开和  c -333
和窥孔优化  c -303
位置无关代码  c -280
记录 353
相对于显示元件 417
相对于 fp  121 , 129 , 412 c -164
关于 sp  c -307
索引  c -72
寄存器间接  c -72
Adobe Systems, Inc.  706 , 724 , 867
Adve,萨里塔五世.  662 , 696 , 698
总计 304
对于数组 371
在作业  c -140
在函数式编程中 538ff
用于初始化 238 , 386
lambda表达式为 542
链接结构 382
列表 400
脚本语言 754
阿霍,阿尔弗雷德五世 40 , 215 , 714 , 806 c -354
阿贾克斯 771
Algol  7、81 请参阅 Algol 60Algol 68;Algol W
声明 134
前向引用  c -27
历史 6
如果构造 226 , 288
嵌套子程序 127
自身(静态)变量 127、136
范围规则 127 , 134
短路评估,缺乏 288
void(简单)类型 303
阿尔戈尔 60246,859
按名称调用参数 433 c -180ff
函数返回值 439
goto语句 247 , 448
输入/输出  c -149
如果构造 110 , 234 , 253
标签参数 248 c -181
循环 266
参数传递 248 , 282 , 433 , 448 c -180ff
递归 468
和 Simula  868
开关构造 246
阿尔戈尔 68301,859
任务 234
赋值运算符 235
并发性 624 , 639
悬垂引用 389
阐述 126
函数返回值 439
如果构造 253
标签参数 248 c -181
不确定性(附带执行)  c -110
正交性 233 , 303
参数传递 248 , 433 c -181
指针 408
参考文献 429
信号量 667
语句和表达式 233 , 252
结构(记录)  351ff
线程 451
类型检查 313 , 407
类型系统 348
unions (变体记录)  357 c -136ff
变量作为引用 231
Algol  W 182,294,301,861
案例陈述 246 , 256
参数传递 424
while循环 275
别名 145ff , 340 , 754
和代码改进 386 c -309; c -312; c -322
定义 145
指针 146 , 386
参考参数 146 , 423 , 434 c -250
C++ 中的引用 428
别名分析 146 , 184 c -310
别名类型。  请参阅 类型、别名
比对 236 , 319 , 354 , 356 , 357 , 368 , 796 c -63; c -141; c -150
HPF阵列 640
艾伦,弗朗西斯 E.  368
艾伦,兰迪  c -332;c -355
alloca(堆栈分配)例程  c -191
阿尔彭,鲍文  c -355
Alpha 架构830、833 c -105​
alpha 转换  c -215
形式语言的字母表 103 c -13ff;c -18
交替 44 , 46 , 58ff , 103ff , 223
Alto 个人电脑 669
歧义 
上下文无关文法 52ff , 80
预测-预测冲突 89
移位-减少冲突 94
AMD 公司 22 c -81
AMD64架构  c -99
美国国家标准协会(ANSI)  211 , 771 , 861–863 , 866 , 868
AND并行性 686 , 695
安德鲁斯,格雷戈里·R.  697 , 869
安卓 418
注释 
在 Java  842ff中
在 RTL  c -276
在语义分析中。  参见 解析树、修饰 参见 语法树、修饰
匿名类型。  请参阅 类型、匿名
C 和 C++ 中的匿名联合  c -138
ANSI。  请参阅 美国国家标准协会
面向对象语言中的拟 497,522ff
抗依赖  c -92
ANTLR (解析器生成器 72、74 c -5
Apache 字节码工程库 (BCEL)  839
专利 363,717,861
动态数组 367
优先权 227
范围规则 125 , 142
阿佩尔,安德鲁W.  41 , 215 , 590 , 806
苹果公司 523 , 722 , 866
68000 模拟器 829 , 854
Rosetta 二进制翻译器 829 , 831 , 832
XCode  25
AppleScript  700、702、724ff
小程序,Java  734ff
应用顺序求值 282ff , 291 , 295 , 567ff , 581 , 623 c -216
Lisp/Scheme  548ff中的应用函数
apt(Java 工具)  845
Apt,Krzysztof R.  c -255
阿拉伯数字 43
架构 217ff c -60ff另请参阅 CISC ; RISC特定架构
ARM 为例  c -80ff
指令集  c -70ff
多处理器 631ff
处理器实现和 824 c -75ff
x86 为例  c -80ff
参数 32 , 35 , 120 , 411 另请参阅 参数、实际
对象闭包中的封装 514
评估顺序 240ff , 282ff , 567ff , 684 , 695
通用 333ff
在 lambda 演算中 581 c -214ff
空间管理 415ff
类型检查 317 , 320
可变大小 365 , 412
阿古斯 137 c -149
白羊座二进制翻译器 831
算术运算。  另请参阅 结合律溢出优先级
在 Prolog  597ff中
算术,计算机  c -65ff
ARM 架构633、834 c -80ffc -99ff
v8(64 位)扩展  c -81;c -85
寻址模式  c -72
原子指令 657
调用序列 418 c -167ff
条件代码  c -73
字节序  c -64
指令编码 796
记忆模型 662
预测  c -74
寄存器设置  c -82ff
阿姆斯特朗,乔 590 , 863
阿诺德,马修 854
数组 359ff
地址计算 371ff
分配 363ff
联想 359 , 377 , 753
界限 360 , 363ff , 373
检查 33 , 184 , 364 , 373 , 385ff
组件类型 359
复合类型 311
符合 364ff
尺寸 363ff
动态 367ff
元素类型 359
全阵列操作 362
索引类型 359
内存布局 368ff 另请参阅 行主布局列主布局dope vector
在 ML  556
多维 360 , 368ff , 755
操作 359ff
衣衫褴褛 370
形状 321 , 363ff
切片 360ff , 718 c -343
稀疏 359 , 410
语法 359ff
ASCII 字符集(美国信息交换标准代码) 
.ascii汇编程序指令 795
和位集 376
和后记 783
在 Prolog  607 , 612
和 Unicode 305、306 c -264
ASM 字节码框架(ObjectWeb)  839
地址分配 796ff
指令 795
指令发射 794ff
宏 162
汇编语言 5ff , 20ff , 33ff , 181 , 246 , 433 , 781 , 792ff c -76
伪汇编符号 218
和类型 298ff
断言 146 , 182ff
在扩展正则表达式 744747770中
作业 12 , 122 , 229ff , 340ff c -48另请参阅 副作用变量的参考模型 变量的值模型
总计 538
数组 362
结合性 228
深 340ff
指称定义 301
表达式 111 , 228ff
函数返回值 438
并初始化 238ff499ff
在 Lisp 中 384 , 725
在 ML  383 , 589
移动任务 431
多路 236 , 755
运营商 234ff
和正交性 234
指针 379 , 391
记录 355 c -140
冗余  c -318
反向分配 511
可逆  c -108
在方案 272 , 545ff中
浅 340ff
在 Smalltalk 中  c -205
字符串 376
类型检查 320 , 323ff
类型推断 324
计算机协会 (ACM)  41 另请参阅 图灵奖
关联列表 545 c -31ff
和关闭 156
动态作用域和 144 , 742
关联数组 359 , 377 , 715
结合性 188 , 286 , 287
任务 236
缓存  c -89
和代码改进  c -310
定义 52
在 lambda 演算中  c -214
运算 224、226ff240 c -205​
AST(抽象语法树)。  请参阅 语法树、抽象
原子 
Lisp  380、398ff
逻辑编程 592ff , 611
ATOM 二进制重写器 833 , 854
原子指令(原始)  654ff
原子操作 627 , 651ff , 664ff , 679
原子变量 662
属性 33 , 180 , 185
在 C++  68中
在 C#  842ff中
评估 187ff
继承 188ff , 254ff c -46ff
空间管理 200ff c -45ff
特设  c -53
合成 187ff , 254 , 288
属性评估器 198
属性语法 3 , 180 , 184ff
代码生成示例 785ff
复制规则 185
装饰(注释)  187ff
错误处理 203
L-归因 192ff
非圆形 190
和一次性编译 193ff
S-归因 188 , 192ff
语义 功能
对于语法树 201ff
翻译方案 191
类型检查示例 201ff
明确 界定
属性堆叠 200 c -45ff
自下而上 200 c -45ff
并解析树  c -50
自上而下 200 c -50ff
CFG 73 中的扩充生产 参见 结束标记、句法
自动递增/递减寻址模式  c -303
AutoCAD  724
自动机理论 103ff , 291 , 407 c -13ff
自动机 
确定性 44 , 407 , 746 , 749 c -13
有限 55 , 94 , 103 , 746 , 749 c -13ff另请参阅 有限自动机
OCaml 示例 565ff
方案示例 548ff
下推式 44 , 103 c -18ff
图灵机 536
可用表达式  c -317; c -351
awk  7,700,714ff , 866
关联数组 715ff
大写示例 715ff
字段 715ff
哈希(关联数组)类型 753
HTML 标头提取示例 714ff
正则表达式 48 , 743
标准化 703
AWT。  请参阅 Java 抽象窗口工具包
选择公理  c -230
公理语义学。  参见 语义学、公理
逻辑编程中的公理 592
艾科克,约翰 854

B

后端 
编译器 26 , 33 , 37 , 102 , 181 , 775ff , 782ff , 783 另请参阅 代码生成代码改进
模拟器  c -189
退避,同步 655
回溯 250 , 848 , 853 c -172
回溯 586 , 599ff , 617 c -108
图标生成器 864 c -108
和并行化 686
Prolog 搜索 599ff , 604ff , 623
递归下降  c -118
巴克斯,约翰·W.  49 , 113 , 589 , 590 , 863
巴科斯范式 49 , 84 , 113 , 859
扩展 49
逻辑语言中的后向链接 598 , 623
向后兼容性 
>C 和 C++  502 , 511
和动态链接 801
Java 泛型 339c - 127
多字节字符 47 , 306
Perl 5 中的作用域 741
在 C 165中单独编译 
x86 架构  c -81; c -84
培根,大卫·F.  410 , 530 c -355
并发任务包模型 635,645c - 344
Bagrodia, Rajive  c -254
巴拉,瓦桑斯 854
鲍尔,托马斯 852 , 855
班纳吉,乌特帕尔  c -355
巴希勒尔,约书亚 113
亨德里克·彼得·巴伦德雷特 590
障碍 
垃圾收集 397
在放松记忆模型中 660 , 816
在 RTL  c -276
同步机制 653 , 655ff , 668
巴顿,克拉拉  c -260
子范围的基(父)类型 309
集合的基(宇宙)类型 376
基类。  参见 类、基类
bash (Bourne - again shell  ) 12,705ff
# !约定 711
内置命令 707
功能 710
异端文档 709
循环 706
管道和重定向 708ff
引用和扩展 709ff
测试、查询和条件 707ff
Basic  8,861请参阅Visual Basic
评论、表现和 19
goto语句 7
范围规则 126
基本块 776,829 c -93 ; c -276 ; c -297; c -304ff; c -312ff
定义  c -93
分析 851 , 855
LR 项目集的基础 92ff , 96
批处理  c -11;c -149
脚本 705
BCEL(Apache 字节码工程库)  839
贝克,利兰 L.  806
贝尔电话实验室 9 , 718 , 846 , 861 , 865 , 868 c -37
贝尔,C.戈登  c -105
贝拉德,法布里斯 854
伯恩斯坦,罗伯特·L.  294
最佳拟合算法 122
贝塔抽象  c -218;c -220
β还原  c -215
B ib T Ec -270; c -272
大端字节顺序。  请参阅 endian-ness
大数(任意精度整数)  753
二进制整数,作为数据格式  c -63
二进制重写 810 , 822 , 833ff
用于调试 846
二分查找 259ff , 286 , 290 , 448
二进制翻译 810 , 822 , 828ff , 854
二进制编码的十进制 (BCD) 算法 307 c -86
数组形状为 363ff
远程程序通信通道 646
定义 116
动态 117 c -31
异常处理程序 441
和程度 156
17 , 23 , 117 , 152 , 300晚 
方法。  请参阅 方法绑定
Lisp c中的名称查找  -33
145ff范围内的名称 
超载 147ff
在 Prolog  599 , 616
引用环境。  请参阅 绑定规则
在方案 542ff中
和范围规则 125ff
静态 117
和存储管理 118ff
绑定规则 126、152ff 155 请参阅包、子程序
深 126,152ff
和动态作用域 152ff
浅 126 , 152
和静态作用域 152
绑定时间 115ff , 154
Birtwistle,Graham M.  469
bison  75 另请参阅 yacc
安德鲁·布莱克 348
暴雪 295
块(句法单位)  7 , 132
定义 125 , 252
和异常处理 441ff
嵌套 134ff , 170 , 172 , 739 , 768
Ruby  250 , 722
在 Smalltalk 中  c -205ff
块复制操作 356
阻塞(数组/循环)  c -337
阻塞(进程/线程)  628、636、647ff664ff c -238ff;c -251另请参阅同步、基于调度程序的定义 657
BMP(Unicode 基本多文种平面)  306
BNF。  参见 Backus-Naur 范式
Bobrow,Daniel G.  410
博赫曼,格雷戈尔 V.  215
身体 
霍恩条款 592 , 595
lambda 抽象  c -216
lambda表达式 541
循环 48262ff266275ff290 c -206;c -323ff;c -333
模块 140 , 486ff c -36
对象方法 508 , 676
远程过程 646 参见 条目,消息传递程序
结构化语句 95
子程序 420 , 438 c -1; c -38
Boehm,Hans  - Juergen 410,698
布尔类型。  请参阅 类型、布尔
Boost 库 526 c -162
引导 21ff , 811 , 827
Borning,Alan H.  530
自下而上解析。  请参阅 自下而上解析
lambda 表达式中的绑定变量  c -215
有界缓冲区 666ff
在 Ada 83  c -245ff中
在阿达 95  675
具有条件临界区域 675
在 Erlang 中  c -247; c -249ff
在 Java 中 677
和消息传递  c -243
带监视器 670
使用信号量 668
具有事务内存 680
界限 
数组 363ff , 373
循环 262ff
伯恩,斯蒂芬R.  705 , 771
拳击 232ff , 286
博耶,罗伯特·S.  770
布拉查,吉拉德 349
分支延迟 419 c -90; c -329
分支指令 246 c -62; c -72ff; c -89ff
和基本块  c -93;c -297;c -304
计算目标 829
有条件 254,263ff c -72ff ; c -75 ; c -102
延迟 795 c -90; c -103; c -303
在 MIPS 架构中  c -73
取消  c -331
在部分执行跟踪 834
流水线和  c -90
在位置无关代码中  c -280; c -284
相对与绝对 795
在 x86 架构中 795 c -86
分支预测 255 c -89;c -96;c -103;c -329;c -331
面向对象语言 529
断点 845ff
Brinch Hansen  Per 669,674
广播 633
监控条件 672
布莱恩特,兰德尔 E.  c -105
好友系统 123
缓冲区溢出攻击 385
缓冲消息传递  c -241ff
shell 中的内置命令 707
内置对象。预定义对象
内置类型。  请参阅 类型、内置
忙等待同步。  请参阅 同步、忙等待
绕过,在宽松的内存模型 660
拜伦,艾达·奥古斯塔伯爵夫人 446
字节码工程库 (Apache BCEL)  839
字节顺序。  请参阅 字节顺序
Common Lisp  827
Perl  828

C

7 , 8 , 12 , 28ff , 294 , 861
= 和 ==  234 , 287
%(模)运算符 345
聚合体 238 , 304 , 538
时代错误  c -6
匿名结构和联合  c -138
算术溢出 38 , 243
数组 321 , 373 , 384 , 405
内存布局 370
作为参数 386
作业 228 , 234
赋值运算符 235
反斜杠字符转义 375ff
位字段 357
布尔类型 234 , 305
break语句 276 , 45 , 358 c -138
ARM 418上的调用序列 
x86 418上的调用序列 
字母大小写 
演员 303 , 318
胁迫 320 , 321
注释,嵌套 55
编译器 20,806 参阅 gcc ; LLVM
_复杂类型 305
并发 编程12,637
条件表达式 226
常数 120 , 426
悬垂引用 388
声明 134 , 386 , 426 c -46; c -54
取消引用(*、 ->)运算符 383
动态语义检查,缺少 318 c -147
动态形状数组 365ff
枚举类型 308
for循环 267
前向引用  c -27
自由程序 124 , 388
函数返回值 439
语法 29
历史  c -37
输入/输出  c -150;c -154ff;c -161
执行情况 20
增量/减量运算符 226 , 235
初始化 238
初始化器。  参见 C,聚合
联子程序 419ff
作为中间形式 20,803
迭代器模拟 274
宏 163 , 175
malloc例程 382
多字节字符 47
嵌套块 170
嵌套子程序,缺少  c -166
非转换类型转换 319
数字常量 105
数字类型 305 , 309 , 344
正交性 303
超载 147ff
参数传递 423ff , 427 , 436
可变数量的参数 436
指针运算 236 , 384
指针 146 , 378ff , 382ff , 408
和数组 322373384ff424
测试后(do)循环 275
优先级 227 , 228
printf  19
引用子程序 432ff439
限制限定符 146
运行时系统 807
范围规则 125 , 132 , 134
另编 137 , 165 , 800
setjmp例程 449ff
短路评估 245
sizeof运算符 387
语句和表达式 164 , 233
静态变量 127 , 136
存储管理 378
严格别名 146
字符串 105 , 344
结构 322 , 351ff
内存布局 357
子程序调用序列 418 c -167ff
子程序作为参数 155
switch语句 260ff
语法错误 102
代币 32 , 45
类型检查 40 , 299 , 313
类型声明,不完整 382
双关语 319
类型系统 353
类型定义 353
未定义行为 33
工会(变体记录)  357 c -160
变量作为值 230
void(简单)类型 303
void* (通用引用) 类型 323
易失性变量 450
C++  7 , 8 , 12 , 15 , 861 另请参阅 C
聚合体 238 , 304
匿名结构和联合  c -138
算术溢出 243
阵列 370 , 405
联想 359 , 361
任务 240
vs 初始​​化 499ff
属性 68
自动声明 326
Boost 库 526 c -162
和 C502,522
C++11  4668148160270309325348358430432501507514538679685698 c -122;c -138;c -162
演员表 319 , 511ff
类 141 另请参阅 C++、对象
嵌套 490
胁迫 322
注释,嵌套 55
编译器,作为预处理器 21
构造函数。  请参阅 C++、对象、初始化
复制构造函数 15 , 499
声明 134
声明类型 326
删除操作 124 , 388
已删除方法 488
析构函数 388392475504ff
枚举类型 309
异常处理 441
朋友 488
函数对象 158 , 514
期货 685
泛型。  请参阅 C++、模板
输入/输出  c -156ff
实现 20,513
联子程序 419ff
接口类  c -197
迭代器对象 268ff , 270
关键词,上下文 46
lambda 表达式 160 , 432 , 538
记忆模型 662 , 679
方法绑定 507
多字节字符 47
多重继承 480 , 521 c -194ff; c -197
名称修改 800 , 806
命名空间机制 137 , 142 c -39
和 .NET  c -286
操作 118 , 297 , 382
数字类型 305
对象 472ff522525
初始化 240 , 475 , 496ff , 502
运算符函数 148 , 225 , 429 , 478 c -157
超载 148ff
参数传递 423 , 427ff , 435ff 另请参阅 C++、参考资料
可变数量的参数 436
指针 378ff
至方法  c -209
智能 392 c -161ff
多态性 302
优先级别 227
r 值引用 430
返回值优化 500
运行时类型识别 853
决议 范围
范围规则 132 , 193
流  c -156ff
字符串 376
结构 351ff , 353
下标 ( [] ) 运算符 361
模板(泛型)  331 , 333ff , 337ff , 483 c -119ff
外部  c -122
可变参数 348
临时对象 499
 
线程 451
类型转换(转换)  319 , 511ff
类型检查 511
单独编译 800
类型推断 325
类型系统 353
类型 ID  853
变量作为值 498
矢量类 367
虚拟方法 508 , 853
类成员的 可见476、491ff526
访问器 459 , 477
算术溢出 243 , 318
阵列 370
联想 361
作为操作员 512
asyncawait  468
属性 842ff
基数 480
bool类型 234
拳击 233
大写 47
演员 323 , 511
类 141 另请参阅 C#、对象
嵌套 490
常数 120
十进制类型 307
声明 134
明确赋值 33 , 184 , 239 , 347 c -287
代表 158 , 432 , 459 , 626 , 644
匿名 158 , 172 , 459
枚举值 309
事件 459
异常处理 323,441
表达式求值 241
扩展方法 494
对象最终确定 505
期货 684
垃圾收集 124 , 378 , 390
泛型 333 , 337ff , 485 c -128ff
制约因素 335
goto语句 246
历史 9 , 637 c -286
索引器 方法361,477ff
初始化 238
接口类 509
迭代器 46 , 268 c -184ff
即时编译器  c -349
关键词,上下文 46
lambda 表达式 159 , 432 , 538 , 626 , 826
林克 625 , 843
记忆模型 662
方法绑定 507
混合继承 516
多字节字符 47
命名空间 137 , 138 , 142 c -40ff
分层(嵌套)  c -41
和 .NET  c -286
操作 297 , 382
可空类型 304
数字类型 305
对象超类 323 , 479
对象 472
初始化 496 , 503 , 504
运算符函数 148
超载 147 , 148 , 309
参数 传递425、435ff436
可变数量的参数 438
多态性 302
优先级别 227
财产机制 459 , 477
反射 611 , 837 , 841
正则表达式 743
运行时系统。  请参阅 通用语言基础结构
范围规则 132 , 193
密封等级 494
单独编译 165 c -40ff
对象序列化 837
字符串 367 , 376
结构 351 , 498
switch语句 257 , 261
同步 674 , 677
语法树 826
任务并行库 (TPL)  626 , 639 , 684
尝试…终于 447
类型检查 313
类型推断 325
工会,缺乏  c -142
通用引用类型 323
无限范围 156 , 158 , 172 , 645
变量作为引用或值 232 , 379 , 498
向量(ArrayList类)  367
虚拟方法 508
类成员的可见性 489
缓存驱逐  c -89
缓存命中  c -63;c -178
缓存行  c -63
缓存未命中 849 c -63ff;c -92ff;c -146ff;c -331ff
缓存 651 c -61ff; c -100
和数组布局 368ff , 374
结合性  c -89
连贯性 633ff , 654 c -176
和同步 691
直接映射  c -89
层次结构  c -61
针对 582 c -337ff;c -340;c -342;c -353进行优化
和并行化  c -344
管道和  c -89ff
仙人掌 454,469
Cailliau,R.  178
通过调用—。  请参见 参数传递,通过—
调用图 170 , 416 , 848
回调函数 160 , 457 , 808
子程序411的调用者 
调用序列 120 , 388 , 411 , 414ff , 434 , 446 , 453 , 807 c -96ff
显示 417
动态链接  c -280ff
和事件处理 457
垃圾收集 391
x86 上的 gcc 418 c -171ff
通用示例 415ff
在线扩展 419ff
ARM 418 c -167ff上的 LLVM 
参数传递 422ff
注册窗口 419ff c -177ff
寄存器,保存/恢复 414ff c -85;c -98
静态链 415
对于虚拟方法 510
剑桥波兰表示法 225 , 280 , 540 另请参阅 前缀表示法
剑桥大学 327,550
Caml  862 另请参阅 OCaml
坎·大卫 582
规范推导。  请参阅 CFL 中的字符串推导
在 lambda 演算c -216中捕获自由变量 
汽车功能 399
卡德利,卢卡 178 , 349 , 865 c -225
基本类型。  参见 类型、基本类型
卡内基梅隆大学 113
CAS。  请参阅 compare_and_swap 指令
级联错误。  请参阅 错误,级联
姓名中字母的大小写 7 , 47 , 107 , 593
case/switch语句 7 , 32 , 74 , 107 , 182 , 246 , 256ff , 294 , 791 c -111
在 C  260ff
有限自动机和 61ff , 65
实现 256ff
强制转换。  请参阅 类型强制转换
CCR。  参见 条件临界区
cdr函数 399
雪松 669 , 865
执行 803
编程环境 41
中央参考表 646 c -31ff
动态作用域和 144 , 742
CFG。  请参阅 上下文无关文法
CFL。  请参阅 上下文无关语言
CFSM。  参见 特征有限状态机
CGI(通用网关接口)脚本 728ff
缺点 729ff
交互式表单示例 728ff
远程机器监控示例 728ff
安全 737
Chaitin, Gregory  -344 年;-355 年
张伯伦,布拉德福德 L.  410
钱伯斯,约翰 718 , 867
钱迪,K.马尼 695
通道,用于消息传递 646 c -235ff;c -244
698教堂 
字符集。  另请参阅 Unicode
本地化 47
多语言 306 , 349
特征数组 376
特征有限状态机(CFSM)  93ff c -12;c -19
epsilon 制作 101
字符,作为数据格式  c -63
校验和 799 , 800
陈浩  c -354
χ(卡方)  c -149
子类。  请参阅派生
儿童套餐  C -41
乔姆斯基,诺姆 103 , 113
乔姆斯基层次结构 103 c -13
诺曼·乔纳基 771
铬 627
丘奇,阿隆佐 11 , 468 , 536ff , 580 , 584 , 590 , 864 c -217
丘奇论纲 536
Church-Rosser 定理  c -217
CIL(通用中间语言)。  请参阅 通用语言基础设施
西尔克 645,862
CISC(复杂指令集计算机)  217 c -70另请参阅 680x0 架构IBM 360/370 架构VAX 架构x86 架构x86-64 架构
数组访问 374
BCD 操作 307
c -78的哲学  ;c -100
寄存器内存架构  c -71
摘要 508ff
基数 128 , 478ff
脆弱基类问题 512
方法,修改 479ff
受保护 488
公共 479
子类。  请参见 派生类
混凝土 509
宣言 476
可执行文件 763
派生 128,478ff
例外情况为 444
层次结构 479ff
模块和 139ff
嵌套 490 , 526
父类。  参见 类、基类
子类。  请参阅 类、派生类
超类。  参见 类、基类
基于类的语言 529
语言分类 11
子句形式 613
逻辑编程和 613
在谓词演算  c -227ff中
克利夫兰,J.克雷格 348
CLI。  请参阅 通用语言基础结构
客户端 Web 脚本 727 , 734ff
时钟寄存器  c -114
CLOS(通用 Lisp对象系统12,472,862
类层次结构 497
元对象协议 (MOP)  854
多重继承 521
模糊度解析度  c -197
对象初始化 504
类型检查 513
类型扩展 491
关闭操作,适用于文件  c -150
封闭范围。  请参阅 封闭范围
Prolog 615ff中的封闭世界假设 
最近嵌套范围规则 127
关闭 
对象 157ff , 294 , 491 , 513ff , 644
一组 LR 项目 92ff , 96
在 Smalltalk 中  c -206
子程序 153ff156、431ff441、621、768、829 c -165 ;c -171c -174​
延续为 250 , 448 , 547
协程为 450
并显示  c -165; c -191
使用动态作用域  c -33ff
作为函数返回值 439
作为参数 434
CLP(约束逻辑编程)  621
CLR(公共语言运行时)  810 c -287另请参阅 公共语言基础结构
862号 
集群(模块)  137 , 486ff
封装 472
goto语句 246
迭代器 268ff , 295
列表 398
多路分配 236
参数传递 425 , 468
递归类型 399
通用引用类型 323
变量作为参考 231 , 295
Clu 137中的集群(模块) 
计算机集群 634
共阵列 Fortran  698 , 863
共同开始 638ff , 642
Cobol  862
十进制类型 307
格式化 I/O  c -152
goto语句 7
历史 9
索引文件  c -151
记录 352
科克,约翰 69 -105;-355
代码生成 27 , 32ff , 34ff , 181 , 198 , 784ff 857; c -297ff
属性语法和 785ff
禁用 102 c -11
表达式 254
GCD 示例 785ff
在单遍编译器中 193
寄存器分配 780 , 787ff
符号表和  c -30
代码生成器 776 , 779 , 806 c -328
“物有所值”  c -348
组合示例  c -305ff
保守派 184 c -310; c -322; c -340
动态 810 , 823 , 831 , 854
和表达式排序 241
全球 777ff , 822 857; c -298; c -312ff
程序间 778 , 822 c -298
在即时编译器中 822
本地 777 , 822 857;c -297;c -304ff
乐观 
窥视孔 857;c -297;c -301ff
c -299ff阶段 
配置文件驱动 824ff
源级 386 c -309;c -324
推测 184
不安全 184 , 824
代码注入  c -176
代码变形 830
强制 150 , 213 , 240 , 312 , 320ff , 703
和超载 174 , 322
类型兼容性和 320ff
COFF(通用目标文件格式)  c -291
科恩,雅克 410
一致性。  请参阅 缓存、一致性 另请参阅 内存模型
Cold Fusion(脚本语言)  731
集合类。  请参阅 容器类
MPI c -256中的集体通信 
阿兰·科尔梅劳尔 591 , 621 , 867
(数组的)列主布局 368ff c -337ff
COM(Microsoft 组件对象模型)  530 , 698
属性的使用 843
组合循环 266ff
组合器,定点 590 c -219;c -223
命令解释器 176 , 700 , 705ff , 718 , 866 另请参阅 shell
MS-DOS 外壳 700
评论 
嵌套 55
性能和基本 19
重要。  请参阅 pragma
公共语言基础设施 (CLI  ) 807、820 c -286ff
建筑概要  c -287ff
调用机制  c -288;c -292
通用中间语言 (CIL)  23783806810821 c -286;c -291
vs Java 字节码  c -291
公共语言规范 (CLS)  c -288ff
通用类型系统 (CTS)  821 c -286ff; c -288ff
代表  c -289
仿制药  c -290
历史 820
vs Java 虚拟机  c -287ff
即时编译  c -287
语言互操作性 820 , 821 c -287; c -292
元数据  c -291ff
操作数堆栈  c -287
指针  c -289ff
安全类型  c -287
不安全代码  c -288
验证  c -288;c -292
vs 验证  c -294
虚拟执行系统 ​​(VES)  c -286
公共语言运行时 (CLR)  810 c -287另请参阅 公共语言基础结构
Common Lisp  25,862 参阅 Lisp
数组 367
布尔常量 544
字母大小写 
接住抛出 465 , 467
CMU 编译器 827
复杂类型 305
异常处理 443 , 465 , 467
lambda表达式 541
循环宏 266
宏 164
多级返回(捕获抛出)  248 , 249 , 291 , 443
嵌套子程序 127
对象。  参见 CLOS
参数传递 435ff
编程环境 9、25ff 41​
有理类型 305
返回-来自 248
范围规则 142 , 412
测序 252
结构 352ff , 381
解除保护 447
更糟即更好 764
CFG 74、79ff的公共前缀 
公共子表达式消除 242 , 286 , 419 c -166; c -297ff; c -302; c -309
全球  c -315ff
通信 427 , 635ff c -343另请参阅 消息传递远程调用远程过程调用
硬件和软件 636
和不确定性  c -112ff
投票  c -244
通信路径  c -239;c -241
compare_and_swap (CAS) 指令 655ff , 682
编译单元 797ff c -40另请参阅 单独编译
编译语言 18
汇编程序和 20ff
后端结构 775ff
绑定时间和 116
编译器系列(套件)  34 , 781 , 782
有条件的 20
动态编译 822ff
函数式语言 279 , 582ff , 684
历史 6
IF(中间形式)  780ff c -273ff
增量 198 , 209 , 215
与计算机架构的交互,xxvff  217ff c -69;c -77;c -88ff;c -98;c -178
语言 设计的交互 xxvff 8、166、285、286、403ff462、689 c -241;附录B
赋值运算符 235
case/switch语句 259ff c -111ff
通信语义  c -241
记录比较 356
动态作用域 143
枚举类型 307
内联子程序 420
后期绑定 118
参数模式 424
指针运算 386
扫描和解析 105 c -20
分号  c -6
副作用 582
子范围类型 310
变体记录  c -141
和解释 17ff , 38 , 117
解释型语言 702
即时 23 , 421 , 512 , 776 , 810 , 822ff , 854 c -349
与解释 811
后期绑定和 23
和现代处理器 36 c -88ff另请参阅 指令调度寄存器分配
通行证 26 , 777 , 780
单程 193ff , 209
阶段 26ff37、776ff780另请参阅扫描解析语义分析代码改进代码生成
查询语言 24
自托管 21
单独。  参见 单独编译
非常规 24
复杂指令集计算机。  参见 CISC
复杂类型。  请参阅 类型、复杂
组件系统,二进制 530
接口查询操作 838
数组的组件类型 359
复合类型。  请参阅 复合类型
复合语句 252
计算机科学课程 2013   xxvi; xxix
计算机, 5ff的历史 
级联 
上下文无关文法 103
列表 283 , 408
在正则表达式中 44ff , 58ff , 103
字符串 367 , 376 c -9
一致性 766
具体语法树。  参见 解析树
并发性 223 , 623ff 另请参阅 通信消息传递多处理器共享内存同步线程
语言 12 , 637ff
图书馆 637ff
消息传递  c -235ff
动机 623,627
多线程程序 627ff
调度循环替代方案 628ff
和不确定性  c -112
vs 并行性 624
进程 635
准平行性 624
竞争条件 626 , 639 , 640 , 650ff , 653 , 664 , 683
共享内存 652ff
任务 635
并发 Haskell 681、685、697、864
并发 Pascal  669 , 673
条件代码 834 c -62;c -72;c -102;c -329
条件队列 649ff , 663ff
监视器 671
条件同步 652 , 666ff
条件变量 
在 Java 中 677
在监视器 670中
条件编译 20
和可执行类声明 763
条件临界区 (CCR)  674ff
与监视器 677
条件移动指令  c -73
配置管理 24 c -279
解析冲突 
預測-預測 89
移位-减少 94
符合数组参数 364ff
合取范式  c -228;c -234
细胞 分裂380
ConScript Unicode 注册表 306
并发系统中的共识 652
因此,霍恩条款 592
保守的代码改进。  请参见 代码改进、保守
保守垃圾收集 397 , 410
一致性。  请参阅 缓存、一致性 另请参阅 内存模型
持续的 
编译时 120
阐述时间 120
字面量 119 , 298 , 322 , 326
清单 120
恒定折叠 804 c -301
恒定传播  c -301
受约束子类型。  请参阅 子类型、受约束
Ada  c -141中的受限变体
基于约束的语言 12
通用参数 335ff的约束
构造类型。  参见 复合类型
建设性证明 537
构造函数 240ff , 297 , 382 , 475 , 495ff 另请参阅 初始化
选择 495
复制构造函数 430 , 499
移动构造函数 430 , 501
超载 
集装箱等级 268 , 323ff , 333 , 484 , 530 c -206
争用,并行系统 654
语境 
在 Perl 中 703719751ff756ff c -47
和类型 297
上下文块 455 , 648ff , 664
上下文切换 628 , 651 , 672 , 674
上下文 无关文法3、29、44ff48ff c -19ff​
模棱两可 52ff , 80
字符串50ff的推导 
左线型  c -24
下级 70 , 73 , 79
LR  70 , 74
49中的非终结符 
分析树和 50ff
解析和 28
生产 
语法和 29
49终端 
树语法和 203
变量 49
上下文无关语言 103 , 209 c -19ff
上下文相关语法 29
上下文特定的前瞻(用于语法错误恢复)  102 c -3ff; c -7
上下文关键词 46 , 64
(数组的)连续布局 368ff
面向对象系统中的逆变  c -129;c -135
控制抽象 115、221、223、411ff 422参阅子程序协同程序延续异常处理;生成器迭代
从延续构建 251
vs 数据抽象 411
在 Smalltalk 中  c -206ff
控制流 221、223ff请参阅并发延续异常处理迭代确定性排序递归选择排序子程序
表达式求值 224ff赋值另请参阅 结合律短路求值优先级
在 Prolog  604ff中
在方案 545ff中
结构化和非结构化 224 , 246ff
转到备选方案 247ff
控制流分析  c -325
控制流图 776 , 829 c -298
边缘分裂  c -319
管道控制危险源  c -89
转换。  请参阅 类型转换
康威,梅尔文 E.  469
库克,罗伯特 P.  144 c -27
LeBlanc-Cook 符号表  c -27ff
Cooper  Keith D. 41,215,806 c -106 ;c -354
协作多线程 650
复制构造函数 15 , 499
复制传播  c -302
复制规则,在属性语法中 185
CORBA(通用对象请求代理体系结构 530、698
核心 632 另请参阅 处理器
康奈尔程序合成器 215
合成器 发生器214,215
协程 450ff , 469 , 648 , 809
上下文块 455
离散事件模拟示例 456 c -187ff
迭代器实现 456 c -183
堆栈 453ff
线程和 452
转移 454ff
库尔塞勒,布鲁诺 215
皮埃尔·雅克·库尔图瓦 698
库西诺,盖伊 862
协方差,在面向对象系统中  c -129;c -135
考克斯,布拉德 J.  865
克雷公司 634 , 697
Crusoe(Transmeta)架构 830
csh (C  shell ) 12,700
CSP(通信顺序 过程182、863、866参阅Go奥卡姆
输入和输出防护  c -254
进程命名  c -235
终止  c -255
CSS(HTML 的层叠样式表语言)  c -259
CTS(通用类型系统)。  请参阅 通用语言基础结构
CTSS(兼容分时系统)  705
CUDA  631,643
库里,哈斯克尔 B.  577 , 590
函数柯里化 328 , 577ff , 621 , 865 c -213; c -221ff
在 Prolog  604中cut (!)
CWI 阿姆斯特丹 720
CYK(Cocke-Younger-Kasami)算法 69
西特龙,罗纳德  c -355

D

DAG。  请参阅 有向无环图
Damron,Peter C.  806
悬垂else问题 80ff , 110
悬垂引用 118 , 124 , 156 , 184 , 378 , 388ff , 389 , 833
锁和钥匙 410 c -146ff
墓碑 410 c -144ff
达林顿,贾里德 L.  614
飞镖 348,771
达特茅斯学院 861
数据抽象 115 , 135ff , 144 , 221 , 333 , 349 , 411 , 471ff , 666 , 669ff c -31; c -76另请参阅 控制抽象继承模块面向对象语言和编程
vs 控制抽象 411
HPF 640中的数据分布 
数据流 685
数据流分析 820 c -225;c -294;c -298;c -312ff;c -316
所有路径 vs 任何路径  c -321
向后  c -317; c -321
前进  c -317
内存中的数据格式  c -63ff另请参阅 类型
管道中的数据危险  c -89
数据成员(字段)  473
数据并行 643 , 655
数据竞赛自由 662ff
数据流语言 11
数据报  c -237
数据记录 621
DCOM(微软分布式组件对象模型)  530
洛林德维尔 113
死代码。  请参阅 说明,无用
死值  c -301
死锁 673ff , 679 , 695 c -112; c -241
调试器 15 , 24 , 144 , 845ff c -27
硬件支持 847
反向 执行
十进制类型。  请参阅 类型、数字、十进制
宣言 
476级 
可执行文件 763
vs 定义 133 , 316 c -37ff
循环索引 266
订购 130ff
嵌套块 134ff
脚本语言 702
声明性语言 11 , 613 , 617 , 620 c -267
语义分析中的修饰。  参见 解析树、语法树修饰、
深度绑定。  请参阅 绑定规则、深度
Lisp c中名称查找的深度绑定  -33
深度比较和分配(副本)  340ff c -250
默认参数。参数,默认
明确赋值 33 , 184 , 239 , 347 , 820 c -287; c -294; c -350
定义与声明 133 , 316 c -37ff
德克,西奥多鲁斯(德克)约瑟夫 653
延迟槽  c -91ff; c -303
延迟分支。  请参阅 分支指令、延迟
延迟负载  c -92; c -103; c -303
delta 还原  c -216
拒绝服务 攻击
非正规数。  参见 次正规数
指称语义学。  参见 指称语义学
美国国防部 9
依赖 625 , 660 , 685 c -91; c -329
反(读写)  685 c -92;c -329
流(写入-读取)  c -92;c -329
环路承载  c -340ff
输出(写入-写入)  685 c -92;c -329
依赖性分析 640 , 683 c -329; c -340; c -355
依赖 DAG  c -329; c -355
弃用的功能  c -258
取消引用左值 231 , 319 , 378 , 382ff
DeRemer,Franklin L.  112
CFL 50ff中字符串的推导 
规范 90ff
最左边 51
最右边 51
派生类。  请参阅 派生类
派生类型。  请参阅 类型、派生
数组的描述符。  请参阅 dope vector
设计模式 293
析构函数 388 , 392 , 446 , 475 , 495 , 504ff 另请参阅 finalization
确定性自动机。  参见 自动机、下推式 另请参见 有限自动机、确定性
确定性并行 Java(DPJ)  697
多伊奇,L.彼得 410 , 530 , 854
开发环境。  请参阅 编程环境
设备、内存层次和  c -61
DFA(确定性有限自动机)。  请参阅 有限自动机、确定性
字典。  请参阅 关联数组
数字设备公司 
二进制翻译工具 830
系统研究中心 865
西部研究实验室 833
迪杰斯特拉,埃兹格 W.  41246294ff468653667669697 c -110ff
维度,数组 363ff
哲学家就餐问题 694ff
迪翁,伯纳德 A.  113
直接映射缓存  c -89
有向无环图 (DAG) 
表达 DAG  c -307; c -329
在指令调度中  c -329; c -355
中间体 780ff c -307
Prolog 中的子句顺序 618
Prolog 示例 600
拓扑排序 618 c -330
指示 
汇编程序 795
vs 提示 420
导演 724
解析器生成器中的消歧规则 52 , 81
离散事件模拟 456 c -187ff
离散类型。  请参阅 离散类型
变体记录  c的判别式-137
歧视联盟  c -140
析取范式  c -234
迪士尼公司 523 , 724
调度循环 628ff
位移寻址模式。  请参阅 寻址模式、位移
显示 417,468 c -163ff ; c -191​
与静态链 417
迪万,阿梅尔 530
DLL(动态链接库)。  请参阅 链接器和动态链接
dll地狱  c -279
执行循环。  请参阅 循环、枚举控制
文档对象模型(  DOM 734、736、765
杜比,朱利安 530
领域 
在指称语义 301 , 306
功能 84 , 580 c -212
多纳休,吉姆 865
涂料载体 364ff , 487c - 209
双精度浮点数  c -63; c -68
德利森,卡雷尔 530
司机 
对于 LL(1) 解析器 83
对于 SLR(1) 解析器 99
用于台式扫描仪 66
DSSSL(SGML 样式表语言)  c -259
DTD(文档类型定义)  738 c -260
鸭子类型 332 , 512
Duesterwald,Evelyn  854
达夫,托马斯 DS  293 , 294
Duff 装置 293 , 294
DWARF 调试标准 846 c -171ff
Dyadkin,Lev. J.  64
动态数组 367ff
动态绑定。  请参阅 绑定、动态
动态编译。  请参阅 编译器和编译、动态编译
动态链接, 子程序120,413
动态链接器 792 , 797 , 800ff , 809 , 830 c -279ff
动态方法绑定。  请参阅 方法和方法绑定、动态
动态优化 810,823,831,854
动态规划  c -14
动态作用域。  请参阅 作用域、动态
动态语义。  请参阅 动态语义
动态类型。  请参阅 类型检查、动态
Dynamo 动态优化器 831 , 854
DynamoRIO  831 , 855

E

伊格尔,迈克尔J.  846 , 855
杰伊·厄尔利 69
Earley 算法 69
提前答复 646 c -254
日蚀 25
ECMA (欧洲标准 机构701、736、771、810、821、861、864 c -286 c -291
ECMAScript  736,864
洛桑联邦理工学院 (EPFL)  867
控制流图c -319中的边分裂 
Efficeon(Transmeta)架构 830
艾希,布伦丹 736 , 864
埃菲尔 7 , 469 , 863
!!(新)运算符 497 , 501
任何超类 479
课程 141
目前 473
延迟特性和类 509
合同设计 183 , 249 , 467
异常处理 467
扩展对象 498、501、504
冰冻507班 
函数返回值 439
仿制药 333 , 485
goto语句 246
方法绑定 507
多重继承 480 , 521 c -197ff; c -202
模糊度解析  c -201
执行情况  c -209
复制  c -199
对象 472
初始化 496ff , 501 , 504
多态性 302
重新命名特征 480 c -197; c -201
反向分配 511
未定义方法 488
变量作为引用或值 232 , 498 , 501
类成员的可见性 489 , 491
详细说明 117,126
线程/任务声明 641
阐述时间常数 120
数组元素类型 359
Elk(Scheme 方言)  701
埃利斯,玛格丽特·A.  530 c -194
else语句,悬垂。  请参阅 悬垂else问题
elsif关键字 82
emacs  16,25,725ff
Emacs  Lisp 701、725ff743
翡翠 348
调用  c -250移动
模拟 830
和效率  c -243
封装 136 , 165 , 177 , 485ff 另请参阅 抽象模块
结束标记,句法 74 , 81 , 88 , 101
字节 319、344、357 c -64ff c -150 ;c -292​
恩格尔弗里特,约斯特 215
恩尼格玛密码 536
恩纳尔斯,罗伯特 696
入口 
消息传递程序  c -235ff
监视器 670
监视器入口队列 671
枚举。  参见 迭代
枚举类型 307ff
枚举控制循环 261ff
环境变量 176
EPIC。  请参阅 显式并行指令集计算
子程序120的尾声 , 414
障碍事件 656
EPS 套装 84 , 88
产量 74 , 95ff
自动机中的 epsilon 转换 56
平等测试 340ff
在公共语言基础设施  c -289
深与浅 340ff
在 ML  329
记录 355
在方案 341544ff中
等式 推理
类型等价。  参见 类型等价
爱立信计算机科学实验室 590 , 863
二郎 537 , 863
有界缓冲区示例  c -247
信息筛选  c -247
进程命名  c -235
接收操作  c -244
发送操作  c -240
终止  c -255
错误程序 33 , 663 c -250
错误生成 103 c -6ff
错误报告 
用于消息传递  c -242ff
反射支持 837
扫描仪支持 54
错误。  另请参阅 语义检查语义错误语法错误
级联 102 , 203 c -1; c -3ff
例外情况 
词汇 65ff
逃逸分析 184 , 498 , 694
字符串中的转义序列 375
埃塔还原  c -215
ETH (Eidgenössische Technische Hochschule),苏黎世 8
以太网 357 , 634 , 669
欧几里得 669,863
别名 176
封装 472 , 486
不变量 183
迭代器对象 268ff
模块类型 139 , 486ff
副作用 242 , 252
和图灵 869
欧几里得算法 5,537 c -111 ; c -207请参阅最大公约数 (GCD)
欧洲研究理事会 868
尤斯塔斯,艾伦 854
Lisp/Scheme  548ff中的eval函数
在函数式编程中 567ff
事件处理 456ff , 658 , 808
执行 情况
用于交互式 I/O  456 c -148
埃维·R·詹姆斯 113
Excel  12 , 620
异常处理 223249ff440ff
作为 goto  249的替代方案
制定时 
和内置错误 33 , 444
清理(​​最后)  446
通信故障  c -251
在子程序头中声明 445
定义异常 444ff
在 Emacs Lisp  726中
用于递归下降  c -5中的错误恢复
异常传播 173 , 445ff
实现 119 , 286 , 447ff , 808
在 OCaml 446、561、562
参数 444
在 PL/I  441
结构化 446
凑合着用 448ff c -182
压制 
执行顺序。  另请参阅 评估顺序
初始化和 496 , 502ff
逻辑编程 598ff604ff613
存在量词 615 c -226ff
退出,从循环 265
扩展对象 498 , 501 , 502 , 504 另请参阅 变量价值模型
脚本语言中的变量扩展 706ff709ff748ff
显式接收。  请参阅 接收操作,显式
显式并行指令集计算 (EPIC)  c -104另请参阅 IA-64 (Itanium) 架构
浮点数  c的指数-67
从模块136 , 476导出名称 
不透明 138
导出表,用于链接 791
表达式。  另请参阅 赋值初始化
评估 224ff
结合性 226ff
优先 226ff
240ff内订购 ,646
参照透明度 230 , 581 c -274
短路 243ff , 254ff , 285 , 287 , 288
vs 声明 224 , 229 , 233
面向表达的语言 233
扩展语言 16700701724ff
商业 724
对象定义的范围 156 , 646 c -191
和 Ada 任务 642
和 C# 代表 156 , 158 , 172 , 432 c -130
和延续 251
和垃圾收集 390 , 538
和高阶函数 577
脚本语言 156 , 718 , 739
外部碎片 122ff , 394 c -146
外部符号 791

F

F

异步让! 468
列表推导 400
和 .NET  c -286
Facebook 公司 348
事实上,在 Prolog  593 , 612
图标305 c -108中的计算失败 
故障导向搜索 614
公平 224 , 649 , 655 , 691 c -114; c -246
假阳性/阴性 683 c -144; c -354
隔离(内存屏障 指令660ff682、816
费尼切尔,罗伯特 R.  410
费马,皮埃尔·德·  c -226;-229
最后定理  c -226; c -229
费尔,艾伦·R  . 294
费斯·罗伯特 590
斐波那契堆 123
斐波那契数 123 , 280ff , 327 , 546
字段 
awk  715
对象 473
记录 352
第五代项目 620
路易斯·恩里克·德·菲格雷多 864
bash  706ff中的文件名扩展
文件 401ff c -148ff另请参阅 I/O
二进制  c -149
复合类型 311
直接  c -151
索引  c -151
内部  c -156
内存映射 792
持续 401 c -149ff
随机存取  c -151
连续  c -150
临时 401 c -150
文本 401 c -149;c -151ff
在 Ada  c -153ff中
在 C  c -154ff中
在 C++ 中  c -156ff
在 Fortran  c -152ff中
终结。  另请参阅 析构函数
垃圾收集和 496 , 504ff
物体 495ff
Findler,罗伯特·布鲁斯 215
有限自动机 55 , 94 , 103 , 746 , 749 c -13ff
计算器令牌 57
确定性 44 , 407 , 746 , 749 c -13
从 NFA 60ff c -16ff创建 
最小化 60
OCaml 示例 565ff
方案示例 548ff
56ff代 
执行 61ff
非确定性 56 , 746 , 749 c -13
回溯 749
通过正则表达式创建 58ff , 746
翻译为正则表达式  c -14ff
有限元分析 655
芬克尔,拉斐尔A.  289 , 295
火狐 627
固件 24 c -76
FIRST 和 FOLLOW 集 84ff88ff94ff c -2ff; c -19ff; c -317ff
首次拟合算法 122
一等值。  另请参阅 函数、高阶
延续 250
定义 155
Prolog 术语 577
子程序 155ff , 299 , 328 , 577
在函数式编程中 538ff
和迭代 272
和元计算 608
售价 718卢比
费舍尔,查尔斯 N.  41 , 103 , 113 , 215 , 410 , 806 c -7
费舍尔,约瑟夫·A.  c -355
函数的不动点 581 c -218; c -225; c -316ff; c -321
固定格式语言 48
定点组合器  c -219ff;c -223
定点类型 305
闪光 724
口味 530
弗莱克,亚瑟·C.  468
flex参见 lex
浮点  c -67ff; c -76; c -90
准确度 243 , 264 , 307 , 317
偏压  c -68
作为数据格式  c -63
双精度  c -63;c -68
指数  c -67
扩展  c -68
小数部分  c -69
逐渐下溢  c -68
历史 399
IEEE 标准239、243、307 c -68 ;c -106 c -155​
和指令调度  c -95
整数转换 317
尾数  c -67
标准化  c -68
科学记数法和  c -67
有效数字  c -67
单精度  c -63;c -68
亚正常(非正常)  c -68
在 x86 架构中  c -69; c -82; c -84
流量依赖性  c -92; c -329
FMQ算法 113c - 7
折叠 576 , 579
跟随 101组
for循环。  请参阅 循环、枚举控制
forall循环。  请参阅 循环、并行
fork操作 638 , 642ff
形式语言  c -14
形式机器。  参见 自动机
形式参数。  请参见 形式参数
正式子程序 431 另请参阅 一等值、子程序
程序格式,词汇 47
格式化输出 
在 Ada  c -153
在 C  c -154ff中
在 C++ 中  c -156ff
在 Emacs Lisp  726中
在 Fortran  19 c -152ff中
第四 8 , 211 , 226 , 782 , 863 , 867
自我表征(同形像)  608
Fortran  12、15、863
聚合体 238 , 304 , 538
数组 321 , 359ff , 362ff , 368 , 405 c -48
动态形状 366
切片 362
任务 234
字母大小写 
共阵列 698
胁迫 321
计算 goto  246
并发 编程12,637
声明 126 c -46; c -54
执行循环 262ff , 264 , 291
等价语句  c -136ff
指数运算 226
格式,固定 48
格式化 I/O  19 c -152ff
函数返回值 439 , 538
goto语句 7 , 246 , 265
历史 6 , 9
输入/输出  c -149;c -156
if语句 253
实现 9,19
内部文件  c -156
多字节字符 47 , 305
嵌套子程序 127
数字类型 305ff
运算符函数 148
并行循环 639
并行性 640 , 683
参数传递 118 , 423ff , 435ff , 463ff
指针 146
优先级别 228
记录 321、351ff357
递归 119ff , 277 , 538
保存语句 127 , 136
扫描 64 , 105
范围规则 126
子程序,库 19
文本 I/O  c -152ff
类型检查 362
类型扩展(对象)  472 , 491 , 522
堡垒 698
逻辑编程中的正向链接 598
前向声明  c -38
前向参考 131 , 193 c -27
正向替换(代码改进)  c -320
FP(语言)  590
浮点数的小数部分  c -69
脆弱基类问题 512
碎裂 122 c -145
外部 122ff , 394 c -146
内部 122 , 453
框架。  请参阅 堆栈框架
帧指针 120 , 121 , 412
Francez,Nissim  c -255
弗雷泽,克里斯托弗 806
空闲列表 122
自由软件基金会 782 另请参阅 bisonemacsflexg++gasgccgdbGENERICGIMPGIMPLEgnatgpcgprofRTL
lambda 表达式中的自由变量  c -215
自由格式语言 47
弗里曼,约翰 178
弗里德曼,丹尼尔 P.  295 , 469
C++ 中的 朋友488
树的边缘 586
前端 
编译器 26 , 33 , 37 , 181 , 775 另请参阅 解析器和解析扫描器和扫描语义分析
模拟器  c -189
x86 或 z 处理器  c -71
函数 411 另请参阅 子程序
bash  710中
柯里化 328 , 577ff c -213
高阶 538 , 576ff , 621 c -213
函数式编程和 576ff
数学  c -212ff
部分  c -212
返回值 120 , 438ff , 538
语义。  参见 语义函数
严格 569
总  c -212
函数构造函数 577 另请参阅 lambda 表达式C#,委托Ruby,块Smalltalk,块
函数对象。  请参阅 对象闭包
函数空间,数学  c -213
函数形式。  参见 高阶函数
函数式语言和编程 11,535ff请参阅Common LispErlangHaskelllambda 演算LispMLOCaml递归SchemeSisal
聚合体 538ff
分配,缺少 391 , 408 , 581
代码改进  c -355
商业用途 539 , 583 , 590
并发 683ff , 695
等式 推理
评估令 567ff
惰性求值 569ff
一等子程序 155ff538ff
垃圾收集 124 , 378 , 538ff
高阶函数 439 , 538 , 576ff
就地突变 582
增量初始化 581
迭代 546
关键概念 537ff
限制 575,581ff
列表 398ff , 538
多态性 538 , 541
递归 538
存储分配 453
理论基础 580 c -212ff
简单更新问题 582
处理器c -75的功能单元 
函子 158 另请参阅 对象闭包
在 Prolog  593中
未来(并行处理)  684
FX!32 二进制翻译器 831 , 854

G

g++(GNU C++ 编译器)  782
加布里埃尔,理查德 P.  764 , 771
加纳帕蒂,马哈德万 806
差距,浮点  c -68
垃圾收集 119 , 124 , 156 , 378 , 389ff , 808
并发 396
保守派 397 , 410
延续 250
和对象的终结 496 , 504ff
在函数式编程中 536 , 538ff
世代 396ff
在热点 409
增量式 396 , 410
标记并清除 393ff , 410
指针反转 394ff
实时执行 390
引用计数 390ff c -146;c -161ff
圆形结构 391
vs 跟踪集合 396
脚本语言 704
停止并复制 394ff , 410
终身任职 408
追踪收藏 393ff , 807
加西亚,罗纳德 349
gas ( GNU汇编程序)  6,794ff c -72
收集操作  c -244;c -249
高斯消元法  c -353
gcc(GNU编译集合 )6、75、468、477、782、806 c -72 ;c -171
_ _packed_ _属性 355
GCD。  请参阅 最大公约数
gdb(GNU 调试器)  845
纳拉因·格哈尼 294
通用编程环境 41
逻辑编程中的生成和测试习语 607
代际垃圾收集 396ff
生成器 274 , 295 c -107ff另请参阅 迭代器
通用(gcc IF)  782
通用子程序或模块 286 , 319 , 333ff c -119ff
协变和逆变  c -129;c -135
实现方案 334ff , 646
隐式实例 338
和宏 163 , 346
和面向对象编程 481ff , 483
参数约束 335ff c -128
和反射 839 , 841 c -127
物化 841 c -128; c -291
类型擦除 839 , 841 c -126ff; c -291
加拉乔鲁,库罗什 696 , 698
吉本斯,菲利普·A·  c -355
吉尔,约瑟夫(Yossi)  530
GIMP(GNU 图像处理程序)  724
459頁 
GIMPLE (gcc IF)  782ff , 806 c -273ff
金格尔,罗伯特 A.  806
金斯伯格,西摩 113
格兰维尔,R.史蒂文 806
全局优化(代码改进)  777ff 857;c -312ff
全局变量 126
在 shell 语言中 706
粘合语言 700 , 701 , 718ff
GLUT(OpenGL实用工具包)  459
gnat(GNU Ada 翻译器)  467 , 782 c -285
GNU。  请参阅 自由软件基金会
前往 863
数组 362
关联数组 359
通信模型 636
函数返回值 439
垃圾收集 378 , 390
接口 516
换行符 48
对象 472
接收操作  c -244
发送操作  c -240
线程 451
类型推断 325
目标,在逻辑编程中 592 , 594ff
目标导向搜索  c -108
哥德尔(语言)  621
戈根,约瑟夫 A.  349
戈德堡,阿黛尔 868
戈德堡,大卫  c -70;c -105
古迪纳夫,约翰·B.  468
Google, Inc.,诉 348 , 771 , 863
戈登,迈克尔 JC  215
戈斯林,詹姆斯 812 , 864
高斯珀,R.威廉 770
goto语句 7 , 246ff , 265 , 290 , 295 , 448
替代方案 247ff , 285
非本地 247
gpc(GNU Pascal 编译器)  782
gprof  848
渐进式打字 348
浮点运算中的渐进下溢  c -68
格雷厄姆,苏珊·L.  806 c -355
语法。  请参阅 属性语法。 另请参阅 上下文无关语法
图形着色和寄存器分配  c -344;c -355
图形用户界面 (GUI)  456 , 523 , 669 , 701 , 807 c -148ff; c -204另请参阅 编程环境
最大公约数(GCD)  13ff , 28ff , 38 , 175 , 278ff , 776ff c -111; c -218ff; c -273ff
汇编语言 5
在 C  13 , 28
机器语言 5
在 OCaml  13中
在 Prolog  13中,618
在 Smalltalk 中  c -207
正则表达式的贪婪匹配 748 , 769
格林斯潘,菲利普 771
grep (Unix)  48 , 743ff
egrep  743
起源 744
大卫·格里斯 215 , 295
格里塞默,罗伯特 863
格里斯沃尔德,玛奇 T.  295
格里斯沃尔德,拉尔夫 E.  295 , 864 , 868 c -118
格鲁尼·迪克 41 , 590 , 806 c -354
Gtk(GIMP 工具包)  459
警卫 
在 Ada选择语句  中c -246
在 SR 中声明  c -113
受保护的命令  c -110ff;c -117;c -245
GUI。  请参阅 图形用户界面
Guile(Scheme 方言701、771
古尔托沃伊·阿列克谢 349
安迪·古特曼斯 866
Guyer,Samuel Z.  c -354

H

黑客 348
句柄,右句形式 90
处理程序,针对事件 457ff
汉森,大卫 R.  468806 c -116
汉森,戴维·海涅迈尔 722
发生在关系 698
哈比森,塞缪尔 P.  177 , 468
哈里斯,蒂姆 692 , 698
哈佛大学 113 , 225 , 363
哈希函数 681 , 800 c -28; c -33
哈希表 64 , 259 , 359 , 377 , 690 c -27另请参阅 关联数组
Haskell  326、537、550、590、864参阅ML
容器等级 575
建造​ 
函子 334
全局变量 575
输入/输出 572ff
缩进和换行 48
中缀运算符 149 , 228
惰性求值 282 , 570 , 586
列表推导 400 , 408 , 575
映射函数 574
可能是304类型 
模块 137 , 138
单子 348 , 572ff
超载 149
参数传递 433
部分函数 575
伪随机数 572ff
副作用,缺乏 571
线程 451
元组 573
类型类别 149 , 329 , 336 , 348 , 554
豪本,迈克尔 771
豪本,隆达 771
海恩斯,克里斯托弗·T.  295 , 469
管道危险  c -89
黑泽尔伍德,金 854
霍恩条款 592
模块140的标题 ,487 c -36
碎片 
空闲列表 122
函数式编程 538
重量级进程 635
安德斯·海尔斯伯格 861
在非阻塞并发算法中提供帮助 658
铁杉  c -149
轩尼诗,约翰 L.  806 c -70; c -89; c -105
亨利,罗伯特 R.  806
709姓氏 
Herlihy,  Maurice P. 680,696ff
赫尔墨斯 347 , 647 c -250
海伦公式 555 , 783
惠普公司 211 , 523 , 633 , 782 , 830 , 831
十六进制表示法  c -65
Heymans,  F.698
高性能 Fortran (HPF)  640 , 683
高阶函数。  请参阅 函数,高阶
辛德,迈克尔  -310 年;-355 年
提示 420 , 676
监控信号为 671ff
历史。附录 A
# !约定 712
抽象机制 471ff
艾达 9
Algol  6的
c -37
C#  9 , 637 c -286
CLI  820
Cobol  9
编译器和编译 6
计算机 5ff c -76ff
浮点数 399
Fortran  6,9
函数式和逻辑编程 536ff
HTML  736
Java  637 , 812
Lisp  6 , 399 , 468
数学和统计语言 717
Perl  741
700菲律宾比索 
PL/I  9
RISC  c -100
范围规则和抽象机制 125ff , 166 , 471
脚本 700ff
单独编译  c -41
Unix 工具 744
何,W.威尔逊 806
霍尔,查尔斯·安东尼·理查德(托尼)  41 , 182 , 215 , 246 , 294 , 295 , 348ff , 669 , 861 , 866
霍尔,格雷登 867
记录布局353ff中的漏洞 
范围内的孔 128 , 132
霍尔特,理查德·C.  697 , 869
同音词语言 547 , 584 , 608 , 617
霍普克罗夫特,约翰 E.  112
霍珀,格雷斯·默里 862
霍恩,阿尔弗雷德 620
霍恩条款 592 c -228
霍洛维茨,埃利斯 41
热路径 824 , 825 , 831
HotSpot  JVM 409、812、825ff
HP 3000 架构 830
HPF。  请参阅 高性能 Fortran
HTML(超文本标记语言)  736ff
和 CGI​​ 脚本 728ff
和客户端脚本 734
标头提取示例 713ff748
awk  714ff中
在 Perl  716ff中
sed  713
作为javadoc  843的输出
作为 XSLT c -262的输出 
和 PHP 脚本 731ff
多语言标记  c -259
和 XML  736ff
HTTP(超文本传输 ​​协议728、765
胡达克,保罗 590
亨特,安德鲁 867
虚拟机管理程序 811 c -85

I

IA-32 EL 二进制转换器 831
IA - 64 (Itanium  )架构633、830、831、834 c -99 c -104
原子指令 655
记忆模型 662
预测  c -74
注册窗口 419 c -177
IA32。  请参阅 x86 架构
IBM 360/370 (z) 架构 
比较和交换 690
字节序  c -64
记忆模型 661
微编程  c -76
IBM 704 架构 119 , 368 , 399
IBM 公司 9 , 861 , 863 , 866
多处理器 633
J.沃森研究中心 368 c -105
IBM CP/CMS  811
伊希比亚,让 859
图标 7 , 864
布尔类型,缺少 305 c -108
发电机 268ff , 274 , 295 c -107ff
模式匹配 559
字符串 375
暂停语句  c -108
ID(语言)  11
IDE(集成开发环境)。  请参阅 编程环境
幂等操作 252
标识符 32 , 45 另请参阅 名称
IEEE 浮点标准。  请参阅 浮点、IEEE 标准
耶路撒冷,罗伯特 864
如果。  参见 中间形式
ILP(指令级并行)  c -90
立即错误检测问题  c -3; c -19
不可变对象 231 , 367 , 425 c -204
命令式语言 11ff
不完全循环嵌套  c -339
实现。  另请参阅 汇编器编译器和编译解释器和解释特定语言和功能
规范 703
编译和解释 17ff , 117
违反直觉 646
语言设计和  xxvff 3,8
处理器架构和 217 , 824 c -75ff
隐式接收。  请参阅 接收操作、隐式
隐式同步 683ff
进口 
名称 放入模块136、476、739 c -39
导入表,用于链接 790
内联子程序 164286419ff476524646824825 c -97另请参阅
按顺序执行 241 c -90
增量编译。  请参阅 编译器和增量编译
增量垃圾收集 410
压痕 48 , 81
数组的索引类型 359
索引,循环 262ff , 285 另请参阅 感应变量
索引寻址模式  c -72
索引顺序文件  c -151
感应变量  c -325ff
工业光魔 724
类型推断。  请参阅 类型推断
Infiniband  634
无限数据结构 283 , 589 c -149另请参阅 惰性求值
中缀表示法 148 , 211 , 225ff , 240
在 ML 226,422
在 Prolog  595 , 607
226,718卢比 ​
在 Smalltalk  226
信息隐藏 135
和反思 838
英格尔斯,丹尼尔 HH  530 , 868
英格曼,彼得 Z.  468
继承 141 , 177 , 471 , 478ff , 485ff
实现 510ff c -194ff;c -209ff
混合 509 , 516ff
模块和 486ff
多个 472 , 521ff c -194ff
歧义  c -196
成本  c -197
修正  c -195ff
在 Perl  759中
重复 522 c -198ff
复制 522 c -199ff
共享 508 , 522 c -199; c -201ff
单 510ff
工会和  c -142
继承的属性。  请参阅 属性、继承
初始化 238ff 另请参阅 赋值构造函数明确赋值终止
和作业 15238ff499ff
复杂 结构
执行令 496 , 502ff
扩展对象 502
物体 495ff
和变量作为引用或 值495,498ff
INMOS 公司 866
输入/输出。  请参阅 I/O
印度信息和 自动化研究所550,862
插入排序 619
检查器-执行器循环转换  c -354
实例化 
隐式,通用子程序 338
Prolog 中的变量 593 , 595 , 599 , 616
指令 调度419、780、795、824、826 c -76 c -90 ;c -95ff c -328ff c -355
寄存器分配和  c -95ff; c -299; c -347
指令集架构(ISA)  810 c -70ff; c -86ff
指令级并行性  c -90
指示 
作为数据格式  c -63
无用  c -303;c -321
Intel 64 架构  c -99另请参阅 x86-64 架构
Intel 公司 22,834 c -81 c -99请参阅x86 架构IA-64(安腾架构)
Interlisp  41 , 590 , 669 c -204
联锁硬件  c -92
中间形式 (IF)  19 , 780ff c -273ff; c -313
机器依赖程度 780 c -305; c -328
基于堆栈 782
互联网度量 859
内部碎片 122 , 453
国际标准化 组织( ISO ) 621、736、810、861、865、866c - 291
互联网地址  c -237
Internet Explorer  736
脚本语言中的变量插值 706ff , 748ff
解释型语言 18
编译 702
口译员和口译 17ff
与仿真 830
错误检查和 18
vs 即时编译 811
后期绑定 17 , 117
超环状 548 , 611 c -225
P 码 21ff
后记 24 , 706
方案 539
和脚本 702 , 712 , 718 , 724 , 731 , 734 , 754 , 764
树遍历 33
与虚拟机 810
过程间代码改进。  请参阅 代码改进、过程间
中断,用于 I/O 事件 457ff
Fortran 363中的内在函数 
自省 837 另请参阅 反射
循环c -323ff中的不变计算 
不变,逻辑 58 , 183
循环 290 , 295
监视器 671
I/O(输入/输出)  401ff807 c -148ff另请参阅 文件事件处理图形用户界面
交互式  c -148ff
monad,在 Haskell  571ff中
流 571ff
iPhone  9,418
ISA(指令集架构)  810 c -70ff;c -86ff
ISO。  参见 国际标准化组织
ISO/IE c -10646标准 47,306 请参阅 Unicode
Itanium。  请参阅 IA-64 (Itanium) 架构
项目,在 LR 语法 91ff中
迭代 223 , 261ff 另请参阅 生成器迭代器
枚举控制 261ff
逻辑 控制261,275ff
递归和 277ff
在 Smalltalk 中  c -205ff
迭代次数 263 , 266 , 291
迭代空间  c -340
迭代器 15268ff286456ff
和延续 274
和协同程序 456 c -183
和一等函数 272
执行情况 599 c -183ff
迭代器对象 270ff c -184
树枚举示例 289
艾弗森,肯尼斯·E.  363 , 771 , 861

J

J

贾维·雅科 178
Java  7、8、12、864另请参阅Java虚拟
注释处理器 845
注释 842ff
小程序 734ff , 783
算术溢出 243
阵列 370
断言 
任务 379
AWT 图形  c -149
布尔类型 234
拳击 233
break语句 276
字节码。  请参阅 Java 虚拟机、字节码
大写 47
演员 323 , 511
课程 141
并发库 676ff
条件变量 677
声明 134
明确赋值 33 , 184 , 239 , 347 , 820
枚举值 309
事件 460
异常处理 323,441
表达式求值 241
期末课程 494 , 507
对象最终确定 505
脆弱基类问题 512
功能接口 162 , 460
垃圾收集 124 , 240 , 378 , 390
泛型 333 , 335 , 336 , 339ff , 485 c -119; c -123ff
制约因素 335
goto语句 246
图形界面  c -148
历史 637,812
实现 8,18,23
初始化 238 , 820
内部类 490
匿名 460
接口类 158 , 509
迭代器对象 268ff
JavaFX 图形 459ff
即时编译器 512 c -349
lambda 表达式 162 , 460 , 538
锁和条件 676ff
消息传递  c -237
方法绑定 507
混合继承 516
监视器 678 , 679
多字节字符 47
操作 297 , 382
数字类型 305
对象闭包 157 , 460 , 491 , 514
对象超类 323 , 479
对象 472
初始化 496 , 503
超载 148 , 309
包装 137、138、142 c -40ff
分层(嵌套)  c -41
参数传递 425 , 436
可变数量的参数 437
移相器 656
多态性 302
优先级别 227
390的实时规范 
递归 类型7,379
反射 611 , 837 , 838ff
正则表达式 743
安全 
范围规则 132 , 193
作用域内存 390
发送操作  c -240
单独编译 165 c -40ff
对象序列化 837
服务小程序 731
字符串 367 , 376
超级 480,503
秋千图形  c -149
同步 674ff , 815
线程池和执行器 645
尝试…终于 447ff
类型检查 183 , 313
类型擦除 839 , 841 c -126ff; c -291
工会,缺乏  c -142
通用引用类型 323
变量作为引用或值 232 , 379 , 402 , 498
矢量类 367
类成员的可见性 489
弱引用 409
Java 建模语言(JML)  844
Java 服务器页面 731
Java 虚拟机 (JVM)  812ff867
建筑摘要 812
存档(.jar)文件 816 c -291
字节码 23 , 211 , 702 , 783 , 806 , 817ff
与通用中间语言  c -291相比
指令集 817
列表插入示例 818
核查 818
类文件 816
vs 公共语言基础结构  c -287ff
常量池 813ff
以及Java 813、820以外的 语言
操作数堆栈 814ff , 817ff
存储管理 813
类型安全 820
JavaBeans  530 , 698
javadoc  842
JavaFX  c -149
JavaScript  12、701、734ff864
数组和哈希 755
一等子程序 739
交互式表单示例 734ff
数字类型 752
对象 757ff , 760ff
属性 755 , 760
原型对象 529 , 757 , 760ff
正则表达式 743
范围规则 739
安全 737
标准化 703
无限 范围
变量作为引用或值 752 , 757
和 XSLT  c -262
贾扎耶里·迈赫迪 215
JCL  700
杰斐逊,大卫 R.  c -193
詹森,约恩  c -180
Jensen 的设备  c -180ff
詹森,凯瑟琳 866
JML(Java 建模语言)  844
Shell 语言中的作业控制 707
约翰逊,斯蒂芬 C.  112
约翰逊,沃尔特 L.  112
约翰斯顿,马克 S.  410
连接操作 638 , 642ff
琼斯,理查德 410
乔丹,米克 865
乔伊·威廉 705
JScript  736 , 864 c -286另请参阅 JavaScript
JSP(Java 服务器页面)  700
跳转代码 254ff
case/switch语句 的跳转表257ff
即时编译。  请参阅 编译器和编译、即时编译
JVM。  请参阅 Java 虚拟机

K

卡尔索,比尔 865
卡萨米,T.  69
凯,艾伦 523 , 868
凯梅尼,约翰 861
肯尼迪,安德鲁 349
肯尼迪,肯  c -332;c -355
布莱恩· 克尼W.41、295、714、861
凯瑟尔斯,JLW  675
钥匙和锁。  参见 锁和钥匙
关键词 45 , 47 , 63特定关键词和语言
语境 46 , 64
预定义名称和 64 , 175
金纳斯利,法案 859
阿隆佐教堂和 537
Kleene 封盖44、58ff103
加上元符号 49
星号 元符号43,46ff
克莱因,格温 215
康托萨纳西斯,列奥尼达一世 693
科恩,大卫·G.  705 , 771
科瓦尔斯基,罗伯特 591 , 621 , 867
科兹拉基斯,克里斯托斯 698
Král,  J.407
肯尼亚先令 705
库尔茨,巴里 L.  215
库尔茨,托马斯 861

大号

L

L-属性属性语法。  请参阅 属性语法、L-属性
左值 230 , 311 , 319 , 323 , 384 c -48
标签 
转到246 
非本地 247
作为 参数248、433、448 c -181ff
LALR 解析。  请参阅 解析器和自下而上的解析
lambda ( λ ) 为空字符串 46
lambda 抽象  c -214
lambda 微积分 11 , 468 , 536 , 580 , 590 , 864 c -214ff
alpha 转换  c -215
算术  c -214;c -223
beta抽象  c -218
β还原  c -215
控制流  c -217ff
柯里化  c -221ff
delta 还原  c -216
埃塔还原  c -215
函数式语言和 11
范围  c -215
结构  c -219ff
lambda 表达式 155 , 159ff , 280 , 304 , 432 , 538 , 541 , 621 , 626 c -205
兰波特,莱斯利·B.  654 , 698
巴特勒 ·兰普森W.669、863、865
兰丁,彼得 178
语言 45 种特定语言
分类 11ff , 533 , 617
并发 12,637ff
基于约束 12
上下文无关 45 , 103 c -19ff
模棱两可的  c -22
数据流 11
声明 11 , 613 , 617 , 620 c -267
设计 7ff
绑定时间和 116
扩展 16 , 724ff
家庭 11ff
正式 103 c -14
免费与固定格式 47
功能 11 , 535ff
胶水 718ff
同像 547 , 584 , 608 , 617
命令式 11ff
实现。  另请参阅 编译器和编译解释器和解释
绑定时间和 116
逻辑 12 , 591ff
非确定性  c -22
面向对象 12 , 13 , 141 , 471ff , 522ff , 757ff
常规 45 , 103
脚本 12,699ff
严格 569
冯·诺依曼 12
落叶松 844
詹姆斯·R·拉鲁斯 698 , 852 , 855
后期绑定。  请参阅 后期绑定
乳胶769 
劳伦斯利弗莫尔国家实验室 684,868
惰性数据结构 283
惰性求值 245 , 282ff , 286 , 295 , 569ff , 586 c -182
定义 283
惰性链接 820 , 823 c -282
lcc(编译器)  806
Lea,Doug,第 691节
叶例程 416 c -165;c -171;c -307;c -346
勒布朗,理查德 J.  410
勒布朗,托马斯 J.  144 c -27
LeBlanc-Cook 符号表  c -27ff
莱德加德,亨利 F.  215
LR生产200 c -49左角 
左分解 80 c -49
CFG 79ff中的左递归 
左线性文法  c -24
最左推导 51
查尔斯·莱瑟森 862
勒多夫,拉斯穆斯 700 , 866
勒罗伊,泽维尔 862
莱斯克,迈克尔·E.  112
刘易斯,菲利普二世 112 , 215
 44 , 62 , 104 , 112
词汇分析。  参见 扫描仪和扫描
词汇错误 65ff
词法作用域。  请参阅 作用域、静态
字典顺序 338 , 362 c -310
李凯 698
库包 1219117433451512797ff807ff c -149另请参阅 链接器和链接、运行时系统
对于并发 637ff 另请参阅 pthread标准 (POSIX)
绑定和对象的生命周期 118
轻量级流程 635
有限范围。  参见 “物体的范围”
林德霍姆,蒂姆 806 , 854
换行符 48
在 Python  720中
Ruby  722
链接器和链接 15 , 19 , 24 , 780 , 797ff , 806
绑定时间和 117
动态 792 , 797 , 800ff , 809 , 822 , 830 c -279ff
懒惰 820,823c - 282​
名称解析 798ff
重新定位和 796ff
单独编译和  c -37
静态 797ff
类型检查和 799ff
Linpack 性能基准 696
林克 625,843c - 296 ​​
林斯·拉斐尔 410
棉绒 40
Linux 操作系统 9、22参阅 Unix
地址空间布局 792
动态链接  c -280
魔法数字 712
利斯科夫,芭芭拉·H·  137 , 177 , 295 , 468 , 469 , 862
聚合体 304
算术运算符 287
任务 384
名称绑定  c -33
关闭 154
条件构造 253
动态类型 398 , 538
平等测试 232 , 340
表达式语法 225 , 422
函数原语 154
历史 6,399,468
如果构造 
必要特征 384
实现 23,827
列表 380 , 398ff , 543
数字类型 305
参数传递 423 , 425 , 436 , 468
多态性 399 , 538
递归 468
递归类型 7 , 381 , 399
反射 837 , 854
S-表达 547 c -274
范围规则 125ff , 132 , 142ff , 543
自我表现(同形像)  17 , 125 , 539 , 547 , 584 , 608
存储分配 468
字符串 376
语法 540
类型检查 183 , 300
无限精度算术 243
变量作为引用 231
列表推导 400 , 408 , 722
列表 379ff , 398ff
复合类型 311
点符号 399
和函数式编程 535 , 538
不恰当的 399 , 597
在 ML  555
符号 399
程序为(方案)  547ff
在 Prolog  596ff中
正确 399 , 543
在方案 543ff中
文字常数。  请参阅 常数、文字
little-endian 字节顺序。  请参阅 endian-ness
实时值 414 c -94; c -302
范围  c -344
分裂  c -347
实时变量分析  c -321; c -333
LiveScript  736
LL 语法 70 , 73 , 79ff c -20ff
LL 语言 80 c -20ff
LL 解析。  请参阅 自上而下解析
劳埃德(Lloyd),约翰·W.  621
LLVM 编译器套件 418、468 c -123 c -167ffc -285
负载平衡 646 c -343
加载延迟 241 c -91ff
负载惩罚  c -91
加载并运行编译 776
load_linked指令 657 , 680 , 691
加载存储架构(RISC 机器)  c -71;c -93
装载机 797
加载、绑定时间和 117
本地代码改进(优化)  777 857;c -304ff
局部变量 126
访问位置  c -62ff;c -97
锁定。  请参阅 互斥, 另请参阅 信号量自旋锁
锁和钥匙(用于悬垂参考检测)  410 c -146ff
逻辑语言和编程 12,591ff 参阅 Prolog
算术 597ff
后向链接 598
子句形式 613 c -227ff
封闭世界假设 615ff
代码改进  c -355
并发 686ff , 695
建设性证明和 537
执行令 598ff , 613
正向链接 598
霍恩条款 592
限制 613ff
列表 596ff
谓词演算 591
反思 611
解决和统一 595ff
理论基础 612 c -226ff
逻辑类型。  请参阅 类型、布尔值
逻辑控制 循环261,275ff
洛梅特,大卫 B.  410
长指令字 (LIW) 架构  c -104
最长可能 标记规则55、64ff
循环 261ff
界限 262ff
组合 266ff
枚举控制 261ff , 291 c -350
索引 262ff , 285 另请参阅 归纳变量
逻辑控制 261 , 275ff , 291
中期测试 276ff
嵌套  c -339
平行 639ff , 683
后测 275
预测试 275
循环不变计算  c -323ff
循环变换  c -323ff;c -332ff;c -355另请参阅 依赖性感应变量循环不变计算
阻断  c -337
分布  c -338
裂变  -338
融合  c -338
检查器-执行器范式  c -354
立交桥  C -337
干扰  c -338
剥皮  c -339
重新排序  c -337
逆转  c -341
倾斜  c -341
软件流水线  c -333ff
分裂  c -338
露天开采  c -343
平铺  c -337
展开 292ff , 824 c -103ff; c -332ff
循环 530
宽松名称等值 315
劳登,肯尼斯 C.  41
LR 语法 70 , 74 c -20ff
LR语言  c -20ff
LR 解析。  请参阅 解析器和自下而上的解析
Lua  12 , 701 , 864
游戏引擎脚本 724
数字类型 752
Lucid 公司 764
卢卡姆,大卫 C.  468
卢恩,汉斯·彼得 211
卢恩公式 211
陆志强 854
Łukasiewicz,  211 年1 月
山猫 803

M

Mac OS  635 , 866
文本文件  c -151
Mach操作系统 635
机器语言 5、20、217、792ff c -70ff​
机器,正式的。  参见 自动机
机器无关的代码改进 27 c -299ff
机器特定的代码改进 27 c -299ff
麦克拉伦,M.唐纳德 468
MacLisp  590
Macromedia 公司 731
宏 162ff , 282ff , 569
并点名 433
和泛型 163 , 346
卫生 163 , 164
与内联子程序 286 , 291 , 420相比
和递归 164
以及 C 436中可变数量的参数 
梅尔森,哈利·G.  349
在 Unix  39中制作工具,800 c -38
malloc(内存分配)例程 382 c -191
恶意软件 835
托管代码 810
显性常数 120
曼森,杰里米 696 , 698
浮点数尾数  c -67
枫 717 , 756 , 771
映射。  请参阅 关联数组
马科蒂,迈克尔 215
水手1号太空探测器 64
标记-清除垃圾收集 393ff , 410
编组。  请参阅 收集操作
马托诺西,玛格丽特 855
Mashey,约翰·R.  705 , 771
麻省理工学院 103 , 137 , 399 , 590 , 645 , 705 , 862 , 868
马萨林,亨利  c -355
数学 717 , 756 , 771
Matlab  684,717,756,771
松本幸弘 (Matz)  722ff , 867
马修斯,雅各布 215
Maurer, Dieter  590 c -209
玛雅 724
可能是类型。  请参阅 选项类型
麦卡锡,约翰 468 , 590 , 864
麦格劳,詹姆斯 R.  868
麦克罗伊,M.道格拉斯 744
麦肯齐,布鲁斯 J.  113
兆字节,定义  c -66
Mellor-Crummey,John M.,第 698 版
面向对象编程中的成员查找 509ff
记忆化 283 , 570 , 582 , 589
内存  c -61另请参阅 对齐字节序数据格式
连贯性。  请参阅 缓存、连贯性
层次结构  c -61ff
布局。特定数据类型
泄漏 124 , 378 , 389 , 393 , 833 c -161
内存型号 659ff , 816
绕过 660
C++11  662 , 679
冲突操作 675 , 680
Java  662 , 678ff , 696
顺序一致性 659ff
时间循环 660ff
水星 597 , 621
合并函数,SSA 形式  c -313ff
梅萨 669ff , 865 c -204
消息传递 687 c -235ff另请参阅 MPIPVM接收操作远程过程调用发送操作
缓冲  c -241ff
命名沟通伙伴  c -235ff
和不确定性  c -112
订购  c -239
偷看  c -247
vs 共享内存 631 , 635ff
元循环解释器 548 , 611 c -225
元计算 608
元数据 807 , 827 , 837 c -291ff
方法和方法绑定 473
摘要 508
基类,修改 479ff
调度 141,508
闭包 157ff , 491 , 513ff , 644
C# 中的扩展方法 494
C# 中的 属性机制459 , 477
重新定义 479 , 507
静态 506
虚拟 508
实现 509ff
vtable(虚拟方法表)  509ff
覆盖 141ff489ff506ff
迈耶·伯特兰 469 , 530 , 863
梅耶罗维奇,Leo A.  215
Michael,Maged M.,第 691 , 696 , 698 节
迈克尔森,格雷格 295 , 590
米奇,唐纳德 295
微代码和微编程 24 c -76ff
微处理器  c -77ff
微软公司 9、348、530、550、861、863另请参阅Active Server Pages;ActiveX C # COM 公共语言基础结构;DCOM DDE F # Internet Explorer.NET OLEPowerShellWindows操作系统;Visual Basic;Visual Studio
编译器 826
微软 Word  783
中期测试循环 276ff
编译器的中端 27 , 181 , 775ff c -273ff
米勒,詹姆斯 S.  806
米尔纳,罗宾 327 , 349 , 550 , 865
米尔顿,唐恩 R.  103 , 113 c -7
正则表达式的最小匹配 717 , 748 , 769
明斯基,马文 410
MIPS 架构 830 c -99ff; c -105
原子指令 657
条件分支  c -73ff
字节序  c -64
指令编码  c -87
记忆模型 661
米兰达 537 , 550 , 865 另请参阅 ML
和 Haskell  864
惰性求值 282 , 570
列表推导 400
参数传递 433
副作用,缺乏 571
米斯拉,贾亚德夫 695
MIT。  参见 麻省理工学院
MITI(日本国际贸易和工业部)  620
混合继承。  请参阅 继承、混合
“混合固定”符号 226 , 422 c -107; c -204
ML  11,621,865参阅F # ;HaskellMiranda OCamlSML
骨料 238,400
阵列 556
作业 383 , 589
柯里化 578
平等测试 340
异常处理 441,446
一等函数 432 , 577
必要特征 383
中缀表示法 226 , 422
列表 398ff , 555
嵌套子程序 127
参数传递 423 , 425
模式匹配 559ff
指针 231 , 380 , 383
多态性 327 , 329 , 399 , 538 , 555
记录 313 , 353 , 557 c -139ff
递归类型 7 , 379 , 381 , 399
范围规则 134 , 412
字符串 376 , 556
元组 236 , 439 , 557 , 578
类型检查 183 , 184 , 313 , 328ff
类型系统 326ff
统一 327 , 330 , 331 , 349
单位(平凡)类型 303
变量作为引用 231
变体类型 379 , 558 c -139ff
手机代码 835ff
运算符 345
模型检查 833 c -354
模数 (1)  81 , 294 , 865
取消引用 (^) 运算符 382
封装 472
goto语句 246
監測 669
参数传递 425
重复循环 275
集合类型 325
Modula  - 2 81,491,865
地址(通用引用)类型 323
字母大小写 
协程 451
一等子程序 156
函数返回值 439 , 538
模块 137ff , 486
数字类型 305
单独编译  c -36
子程序作为参数 155
Modula-3  865
阵列 360
字母大小写 
一等子程序 156
前向引用  c -27
执行 803
模块 137 , 138
对象初始化 497ff
超载 147
参数传递 468
指针 378
refany(通用引用)类型 323
范围规则 132
另编 165 c -36
同步 
线程 644
终于尝试了 447
类型检查 313
类型扩展(对象)  141 , 491 , 521
模块 135ff , 486ff
封闭 范围
出口 136
通用。  请参阅 通用子程序或模块
标题 487
进口 136
信息隐藏 135
担任经理 138ff
开放范围 138
并单独编译  c -36ff
参数 487ff
类型 139ff , 471 , 487
单子 348 , 571ff , 621
监视器 182,669ff
有界缓冲区示例 670
条件变量 670
vs 条件临界区 677
Java  678,679
消息传递模型  c -254
嵌套调用 673
语义 671ff
信号作为暗示和绝对 671ff , 674
单 声道459、783、821c -286 -293
摩尔,查尔斯 H.  863
摩尔,戈登 E.  c -78
摩尔,J.斯特罗瑟 770
莫斯,J.艾略特B.  680 , 698
莫特瓦尼,拉吉夫 112
移动语义 392 , 430 , 501
集体通信  c -256
进程命名  c -235
接收操作  c -244
还原  c -256
发送操作  c -240
MSIL(Microsoft 中间语言)  c -287另请参阅 公共语言基础结构公共中间语言
穆奇尼克,史蒂文·S.  806 c -106;c -355ff
多核处理器 624、625、632 c -60 ;c -78ffc -100​
多路复用器 705
多维数组 368ff , 755
多级返回、goto和 247ff
多语言字符集 306、349 参阅 Unicode
语言 684,695
多遍编译器 207 另请参阅 编译器和编译、遍历、单遍
多重继承。  请参阅 继承、多重
多处理器 533
建筑 631ff
缓存 一致性632、633ff654、691 c -176​
调度 651
单芯片 624 , 625 , 632 c -60; c -78ff; c -100
均匀与非均匀记忆 631
矢量。  请参阅 矢量处理器
多线程 627ff
合作社 650
调度循环替代方案 628ff
在硬件中 632 c -60; c -78ff
需要 627ff
先发制人 650
多路分配 236 , 755
穆勒,斯蒂芬 469
互斥 627 , 652 另请参阅 同步
在监视器 671 , 673中
步行锁 678 , 692
相互递归 131
相互递归类型 380
mx二进制 翻译830,854
Myhrhaug  Bjørn 177,868

N

类型名称等价性 313ff , 799
严格与宽松 315
名称修改 800 , 806
命名(关键字)参数 435ff
名称 3,115参阅 绑定绑定规则通信,命名合作伙伴关键字范围
预定义 47 , 64 , 128 , 175
合格 128 , 723 , 838 c -39ff; c -48
范围解析运算符 128
脚本语言 739ff
命名空间 
在 C++ 中 137 , 142 c -39
在 C#  137 , 142 c -40ff中
在 Perl  757中
脚本语言 739
在 XML 中  c -261; c -264
NaN(不是数字),浮点  c -69
诺顿,帕特里克 812
Naur, Peter  49 , 113 , 859 另请参阅 Backus-Naur 形式
乔治·内库拉 853
Prolog 615ff中的否定 
纳尔逊,布鲁斯 698
尼尔森,格雷格 865
嵌套监视器问题 673
嵌套 
134ff542ff区块 
扫描仪62ff中的案例陈述 
评论 数 55
上下文无关文法 48
监控呼叫 673
子程序 127ff468、739ff
访问非本地对象 128
实现。  请参阅 调用序列 另请参阅 显示静态链接
脚本语言中的局部变量 739ff
.NET  530 , 783 , 810 , 861 , 863 另请参阅 C# ; COM ;公共语言基础结构; F#
编译器 826
林克 625,843c - 296 ​​
任务并行库 (TPL)  626 , 639 , 684
Windows 演示基础 (WPF)  459
NetBeans  25
Netscape 公司 701 , 736
纽约大学 (NYU)  214 , 409
纽厄尔·艾伦  c -105
NeXT 软件公司 865
NFA(非确定性有限自动机)。  请参阅 有限自动机、非确定性
蚕食 307
尼普科夫,托拜厄斯 215
无需等待发送  c -240
非绑定预取 184
非阻塞算法 657ff , 696
非转换类型转换。  请参阅 类型转换、非转换
不确定性 224 , 283 c -110ff
非确定性自动机。  参见 自动机、下推。 另请参见 有限自动机、非确定性
非歧视工会  c -140
非本地对象,访问 128
非终结符,属于 CFG  49
扫描64中的非平凡前缀问题 
nop(无操作)  794 c -74;c -92
正常顺序求值 282ff , 295 , 567ff 另请参阅 惰性求值
在 lambda 演算中 581 c -216ff; c -220
宏和 291 , 433
和短路 584
正常化 
浮点数  c -68
代码改进的语法树  c -310
挪威计算中心 177 , 472 , 868
符号。  另请参阅 正则表达式上下文无关文法
剑桥波兰语 225 , 280 , 540 另请参阅 前缀符号
建设性 
列表 399
伪组装 218
无效分支。  请参阅 分支指令、无效分支
数字,阿拉伯语 43
数字运算。  请参阅 算术运算
数字类型。  请参阅 类型、数字
Numrich,罗伯特 697
克里斯汀·尼加德 177 , 472 , 868

O

奥哈拉隆,大卫  c -105
橡木 812
天卫四 81 , 294 , 865
模块 140
对象初始化 497ff
类型扩展(对象)  141 , 491 , 521 c -142
对象闭包 157ff , 294 , 432 , 457 , 491 , 513ff , 644
目标代码 17 另请参阅 机器语言
二进制翻译 828
可执行文件 790
可重新定位 790
对象管理组 530
基于对象的语言 529
面向对象语言和编程 1213116141471ff522ff757ff 另请参阅 抽象构造函数容器类析构函数继承方法和方法绑定特定的面向对象语言
拟人化 522ff
基于类的语言 vs 基于对象的语言 529 , 760
代码改进  c -355
协变和逆变  c -129;c -135
动态方法绑定 505ff
封装和继承 485ff
泛型和 481ff
初始化和终止 495ff
模块和 139ff
多重继承 521ff c -194ff
脚本语言 757ff
统一对象模型 522
成员可见性 479 , 488 c -27; c -30
Objective-C  472 , 865
类别 512
类层次结构 497
iPhone 和 iPad  9
方法绑定 507
混合继承 516
对象初始化 504
对象超类 479
多态性 302
自我 473
超级 480
类型检查 332 , 513
类成员的可见性 489
ObjectWeb ASM 字节码框架 839
课程 141
DFA 示例 565ff
平等测试 329
评估令 567ff
泛型(函子)  334
制约因素 335
lambda 表达式 159
惰性求值 571
对象 472 , 521
选项类型 303
多态性 302
递归类型 379
统一 
Occam  866 另请参阅 CSP
频道  c -236
输入和输出防护  c -254
内存模型 640
接收操作  c -244
远程调用  c -240
发送操作  c -240
终止  c -255
奥德斯基,马丁 867
奥格登,威廉 F.  215
OLE(Microsoft 对象链接和编辑系统)  530
Olivetti 研究中心 865
奥尔森,罗纳德 A.  806
单遍编译器 193ff , 209
蛋白石  c -149
不透明类型和导出。  请参阅 导出、不透明
开放网络计算 (ONC) RPC 标准 698
打开操作,适用于文件  c -150
开放范围。  参见 范围、开放
开源 
ANTLR  72
Apache 字节码工程库 839
重击 705
CMU Common Lisp 编译器 827
日蚀 25
海湾合作委员会 782
 845版本
HotSpot JVM  812 , 825
语言设计和 8 , 704 , 764
Linux  9
单 声道783,821c - 286
NetBeans  25
ObjectWeb ASMbytecode 框架 839
引脚二进制重写器 834 , 852 , 854
Python  720
R 脚本语言 718 , 867
剑麻 868
强谈 530
Valgrind  176
OpenCL  631 , 643
OpenMP  638
639ff指令 
并行循环 639ff
减少 
线程创建 638
运算符 224 另请参阅 表达式
任务 234ff
结合性 52
优先权 
谓词演算 612 c -226
运算符优先级解析 112
OPS5  621
皓龙处理器  c -81
乐观代码改进 184
优化  c -355另请参阅 代码改进
选项类型 303 , 310 , 566
并行 686 , 695
Oracle Corporation  633 另请参阅 Sun Microsystems, Inc.
HotSpot  JVM 409、812、825ff
在 Prolog  598ff中
在表达式 240ff内
数学恒等式 242ff
序数类型。  请参阅 离散类型
枚举常量的序数值 308
正交性 233ff , 302ff , 817
奥斯特豪特,约翰 K.  665 , 701 , 771 , 869
无序执行  c -78;c -89ff;c -92
输出依赖性  c -92; c -329
溢出,在二进制算术中 33 , 38 , 243 , 285 , 286 , 318 c -62; c -66; c -72; c -101
重载 147ff , 309 , 349
在 C++  148ff中
和胁迫 174 , 322
构造函数 475
定义 145
覆盖方法定义 141ff489ff506ff

P

P 码 21ff , 211 , 783 , 811
记忆模型 661
分割 791
包装类型。  请参阅 包装类型
页面错误 791
恐慌模式。  请参阅 语法错误、恐慌模式恢复
并行性 624 另请参阅 并发性
粗粒  c -343
vs 并发 624
数据并行 643 , 655
细粒  c -343
粒度 624 , 625
指令级 625 c -355另请参阅 指令调度
循环级别 639ff c -342ff
任务并行 643
线程级 625
矢量 625 , 640 , 683
并行化  c -342ff; c -355
Parallels Desktop (VMM)  811
参数传递 422ff 另请参阅 参数
在 Ada  427ff中
C 386中的数组 
通过关闭 431ff
移动  c -250
按名称 282 , 433 , 569 c -180ff; c -216
按需 433 , 571 , 718 c -182
参考 118 , 423 另请参阅 C++, 参考
425ff的歧义 
按结果 427
分享 425
按价值 423
可变数量的参数 15 , 436ff c -149
参数。  另请参阅 参数传递
实际 411 , 422ff 另请参阅 参数
C++ 引用 427ff
关闭为 431ff
符合阵列 364ff
const(只读)  426 , 428
例外情况 
正式 411 , 422ff
泛型 333ff
标签 248 , 433 , 448 c -181ff
在 Lisp/Scheme  422
模式 423ff
命名(关键字)  435ff c -153
可选(默认)  433ff
位置 435ff
远程程序  c -250
子程序为 576ff
这个 487ff
类型检查 317 , 320
参数多态性。  参见 多态性、参数
ParcPlace Smalltalk  854
父类型。  参见 类、基类
括号 
在算术表达式中 227 , 242
数组下标 359
在 Lisp/Scheme 中 225 , 379 , 399 , 540
在 ML 元组中 578
在 Prolog  593中
正则表达式 46
Ruby  722
在 shell 语言中 710
帕洛格 621,686
帕纳斯,大卫L.  177 , 698
Parrot 虚拟机 828
解析表 
下级 74 , 84
LR  95ff
分析树(具体语法树)  28ff , 180ff
歧义和 52ff
结合性和 52ff
和属性堆栈  c -50
自动构建 213
180ff187ff的修饰(注释) 
衍生品和 50ff
LL 或 LR 解析过程中的探索 70ff , 89
GCD 示例 29ff
优先级和 52ff
语义分析和 181
求和平均示例 78
解析器 生成74、94、104参阅ANTLR;yacc
自下而上 (LR,移位-减少)  70 , 89ff
属性和 200
规范推导 90ff
CFSM  93ff
epsilon 制作 95ff
拉拉 72,94,112c - 20ff
LR 物品 91ff
LR(0)  94
单反 72 , 94 , 112 c -20ff
表驱动 95ff
变体 93ff c -20ff
复杂性 69
CYK(Cocke-Younger-Kasami)算法 69
Earley 算法 69
运算符优先级 112
语法制导翻译 69
自上而下(LL,预测)  70,82ff 请参阅 递归下降
属性和 200
EPS 套装 88
FIRST 和 FOLLOW 集 84ff88ff c -2ff;c -19ff
PREDICT 集合 84ff , 88
在方案 587中
超视距  C -20
表格驱动 82ff
部分执行跟踪 831 , 834 , 848 另请参阅 跟踪调度
部分冗余  c -320
分区全局地址空间 (PGAS) 语言 698
帕斯卡 7 , 8 , 294 , 866 c -2
算术溢出 243
字母大小写 
案件陈述 259
胁迫 321
评论 105 , 107
编译器 21
一致的数组参数 365
声明语法  c -54
取消引用 (^) 运算符 382
动态语义检查 410 c -147
早期成功 22
枚举类型 307
和欧几里得 863
前向引用  c -27
函数返回值 439 , 538
GNU 编译器 (gpc)  782
goto语句 247 , 448
输入/输出  c -149;c -156
if语句 80 , 105 , 110 , 253 c -6
实现 8,21,783
局部物体的有限范围  c -191
运算符 345
填充类型 464
参数传递 424ff
指针 378ff
优先级别 228
重复循环 275
范围 规则
分号  c -6
集合类型 311 , 325 , 345 , 376
短路评估,缺少 244 , 254
标准化 8
陈述和表达式 233
字符串 107
子范围类型 309
类型检查 313 , 315
类型兼容性 320
类型系统 348
变体记录  c -160
帕斯卡三角形 767ff c -305
编译器通过 26 , 777 , 780
沟通路径。  参见 沟通路径
模式匹配。  另请参阅 正则表达式
awk  714ff中
贪婪 748,769
在 Icon  274 , 295 c -107ff中
最小 717 , 748 , 769
在 ML  559ff中
脚本语言 559 , 704 , 743ff
帕特森,大卫·A·  -70;-89;-105
PDA。  参见 自动机、下推式
PDF(便携式文档格式)  c -151
PE(可移植可执行)程序集 821 c -286;c -291
偷看 
内部消息  c -247
窥孔优化 857;c -297;c -301ff
奔腾处理器  c -75;c -81;c -99另请参阅 x86 架构
完美循环嵌套  c -339
性能分析 24 , 632 , 848ff
性能计数器,硬件 849
性能与安全性 182 , 241 , 285 c -325
数组 753
祝福机制 757
字节码 828
规范实现 703
字母大小写 
和 CGI​​ 脚本 728
胁迫 
上下文,使用 703719751ff756ff c -47
表达 经济
错误检查 751
一等子程序 739
“强制退出”示例 718ff
哈希 753
HTML 标头提取示例 716ff
输入/输出 716c - 152
继承 
最后声明 
模块(包)  137 , 719 , 754 , 757
座右铭 716
多路分配 236
嵌套子程序 739 , 768
数字类型 753
模式匹配 559
和菲律宾比索 866
正则表达式 48717743745ff
远程机器监控示例 728ff
范围规则 142702739ff741ff
选择性混叠 754
严格“vars”模式 739
语法树 827
污点模式 853
绑定变量 176
类型团 754
无限 范围
变量作为值 752 , 757
更糟即更好 764
Perl 6 
即时编译 828
面向对象 720 , 757
perlcc  828
Perles,Micha A.  113
持久文件 401 c -149ff
彼得森,加里·L.  653 , 698
佩顿·琼斯,西蒙。L.  590
PGAS(分区全局地址空间)语言 698
pH(语言)  537 , 546 , 582 , 683 , 697 , 864
编译阶段 26ff , 37 , 776ff , 780 c -299ff另请参阅 扫描解析语义分析代码改进代码生成
φ (phi) 函数,SSA 形式  c -313ff
数组和哈希 755
异常处理 441
交互式表单示例 732ff
模块(命名空间)  137
嵌套子程序,缺少 739
数字类型 752
对象 757ff , 760ff
参考文献 467
正则表达式 743
远程机器监控示例 731ff
范围规则 702 , 739
自发帖 732ff
服务器端 Web 脚本 731ff
类型检查 703
变量作为引用或值 752 , 757
和 XSLT  c -262
短语级语法错误 恢复102、113 c -2ff ;c -10ff
PIC(位置无关代码)  c -279;c -280ff
皮尔斯,本杰明 C.  348 c -225
鸽巢原理  c -19
搭载消息  c -243
派克,罗伯特 863
引脚二进制重写器 834 , 852 , 854
管道停顿  c -75;c -89ff
流水线  c -75;c -89ff;c -100另请参阅 分支预测指令调度
分支机构和  c -90
缓存和  c -89ff
脚本语言中的管道 708ff
PL/I  866
十进制类型 307
异常处理 441 , 468
一流标签 448
格式化 I/O  c -152
历史 9
索引文件  c -151
标签参数 248
指针 377
子程序作为参数,缺少 155
Plauger,Phillip J.  295
插件模块,用于 嵌入式脚本724、734
指针分析  c -310
指针运算 182 , 384
垃圾回收中的指针反转 394ff
指针调换  c -285
指针 377ff 另请参阅 悬垂引用;变量的垃圾收集引用模型
和地址 378
别名和 146
和 C 中的数组 384ff , 424
复合类型 311
帧指针 412
执行 情况
在 ML  383
操作 378ff
以及 C 语言中的参数传递 424
语义分析和 33
智能 391C - 161
堆栈指针 412
到 C 432中的子程序 
语法 378ff
波拉克,W.  469
波兰后缀表示法 211ff 另请参阅 后缀表示法
轮询通信  c -244
多语言标记,在 HTML  c -259
多态性 118 , 150 , 299ff , 322 , 327 , 349 , 380 , 481ff , 535
特设 
函数式编程和 538
在 ML  555
参数 302 , 331ff
显式 333ff 另请参阅 通用子程序或模块
隐式 327 , 329 , 332 , 535
在方案 541中
子类型 302 , 505 另请参阅 方法和方法绑定、动态
类型系统和 302ff
端口,消息传递程序  c -235
可移植性 8
字符集 47
Java  285
非转换类型转换 319
数值精度 39 , 305
Unix  9
虚拟机 811
位置无关代码 (PIC)  c -279; c -280ff
位置参数 435ff
POSIX  c -191
pthread标准 453
正则表达式 743ff
选择例程  c -118
外壳 703 , 705
波斯特,埃米尔 536
测试后循环 275
子程序的后置条件 183
后缀表示法 211ff , 225 , 287
后记 24 , 211 , 226 , 706ff , 782 , 783 , 867 c -151
普津,路易斯 705
电源架构 264、633、780、829 c -99ffc -105ff​
寻址模式  c -72
AltiVec 扩展 695
原子指令 657
条件代码  c -73
条件确定延迟  c -103
十进制算术 307
字节序  c -64
记忆模型 662
幂集(数学中)  c -213
PowerShell  700、724、731
pragma(编译器指令)  67ff419444638842 c -160另请参阅 Java 中的注释C++ 和 C# 中的属性
预测试循环 275
优先 287 , 422
在 CFG  32 , 52 , 184
定义 52
在 lambda 演算中  c -214
运算符 163224226ff240282 c -205
子程序的前提 条件
预定义名称 47 , 128
关键词和 64 , 175
预定义类型。  请参阅 类型、预定义
谓词 
在逻辑上 612 c -226
逻辑编程 592
谓词 演算591,612c - 226ff
斯科莱姆化  c -227;c -230ff
预测  c -74
PREDICT 集合 84ff , 88
预测-预测冲突 89
预测解析。  请参阅 自上而下解析
抢占 628 , 648ff
预取 184
Emacs Lisp 中的前缀参数 726
前缀表示法 225 , 287 , 607 另请参阅 剑桥波兰表示法
CFL c -20的前缀属性 
预处理器 19 , 20 , 24
C++ 编译器为 21
漂亮的打印机 24
主缓存  c -61
原始类型。  请参阅 类型、原始
普林斯顿大学 537 , 865
优先级队列 122
476ff类的私有成员 
私有类型,在 Ada  336中
程序抽象 223 另请参阅 控制抽象
程序 411 另请参阅 子程序
进程 635 , 647
轻量级和 重量级
线程和 635 , 647
处理器 632 另请参阅 架构微处理器多处理器矢量处理器
为现代处理器编译 36 c -88ff另请参阅 指令调度寄存器分配
定义 632
处理器状态寄存器  c -62;c -72
处理器/内存差距  c -62
生产,CFG  49
Proebsting,Todd A.  c -354
Proebsting 定律  c -354
教授 848
配置文件驱动的代码改进 824ff
分析器 24 , 834 , 848ff
基本块 851 , 855
程序计数器(PC)  c -62
程序维护 7
编程环境 24ff
41类 
对于 Smalltalk  25ff
Prolog  7、12、535、593ff 621、867​
算术 597ff
原子 593
回溯 599ff
调用谓词 605 , 611 c -226
字符集 607
子句谓语 612ff
封闭世界假设 615ff
控制流 604ff
切(!)  604
数据库 593
操纵 607ff
执行顺序 598ff604ff613
事实 593 , 612
失败谓词 605
故障导向搜索 614
函子 593 , 610ff
发电机 605ff
进球 594ff
霍恩条款 593ff
实现 23 , 598ff , 613ff
无限回归 600ff , 619
实例化 593 , 599 , 616
列表 596ff
循环 605
否定 615ff
\+ (非)谓词 602ff , 613 , 615ff c -229
并行化 686
括号 593
查询 594 , 598
递归类型 399
反思 611 , 837
第595ff号决议 
规则 593
检索策略替代方案 614 , 619
选择 605
自我表征(同形像)  17 , 608
结构 593
条款 593
井字游戏示例 600ff , 605 , 608ff
类型检查 593
统一 331 , 593 , 595ff , 599
变量 593 , 595
子程序120的序言 , 414
承诺 
语言名称的发音 7
证明,建设性与非建设性 537 , 617
带证明代码 853
正确列表。  参见 列表,正确
C# 中的属性机制 477
谓词演算中的命题 612 c -226
受保护的基类 488
488类受保护成员 
Ada 中的受保护对象 674
JavaScript 的原型对象529、757、760ff
伪汇编符号 218
伪指令 795
伪随机数 252 , 572 c -114
pthread标准 (POSIX)  453
ptrace  846
公共基类 479
476类公共成员 
Pugh,威廉 698
泵引理 113
下推式自动机。  参见 自动机、下推式
PVM(并行虚拟机)  698
阵列 754
引用环境的绑定 154
规范实现 703
字母大小写 
类层次结构 497
已删除方法 488
平等测试 340
异常处理 441
可执行类声明 763
作为扩展语言 701
一等函数 739
“强制退出”示例 720ff
哈希 754
实现 8
缩进和换行 48 , 720
解释 18
迭代器 268 , 269
lambda 表达式 538
列表推导 400 , 704
列表类型 399
方法绑定 507
方法调用语法 491
模块 138、142、719
多维数组 755
多重继承 521 c -197
多路分配 236 , 755
嵌套子程序 127
数字类型 719 , 753
参数传递 435ff
多态性 302
反思 611
正则表达式 48 , 720 , 743
范围规则 132702739ff
377 , 755套 
线程 451
尝试…终于 447
元组 439 , 755
类型检查 183 , 300 , 332 , 703 , 751
无限 范围
变量作为参考 498 , 752 , 757
类成员的可见性 490
Python 软件基础 720

Q

QEMU二进制翻译和虚拟化系统 831 , 854
四倍 
限定名称 128 , 723 , 838 c -39ff; c -48
量词 612 , 615ff c -226ff
准平行性 624
询问 
bash  707ff中
在 Prolog  594 , 598 c -228ff中
查询语言处理器 24
快速排序 408 , 614 , 619
Quine,Willard Van Orman  225
Quiring ,Sam  B. 103,113 c -7
在 shell 语言中引用 709ff

R

R

R 脚本语言 718 , 867
阵列 718
自动并行化 684
需要致电 571
一等子程序 739
中缀表示法 226
多维数组 756
参数传递 433 c -182
范围规则 739ff
超级任务 740
无限 范围
r 值 230 , 384 c -48
参考文献 430
拉宾,迈克尔·O.  113
阿隆佐教堂和 537
竞争条件 626 , 639 , 640 , 650ff , 653 , 664 , 683
参差不齐的阵列 370
拉格斯代尔,苏珊 806
拉杰瓦尔,拉维 698
兰德尔·布赖恩 468
随机数,伪 252 , 572 c -114
随机访问文件  c -151
函数范围 580 c -212
有理类型。  参见 类型、数字、有理
Rau,B. Ramakrishna  c -355
达到定义  c -312;c -324;c -344
读取-求值-打印循环 539ff
读-修改-写指令 654ff
只读参数 426
读写依赖。  参见 依赖、反
就绪列表 649ff , 653 , 663ff , 690 c -251
Java 390的实时规范 
接收操作  c -244ff
明确  c -244ff;c -249
隐式 646 c -244ff;c -249;c -251
偷看  c -247
记录 351ff 另请参阅 变体记录
任务 355
比较 355
复合类型 310 , 557
字段 352
订购 356
内存布局 353ff , 646
孔 353ff
在 ML  557
嵌套 352
符号表管理  c -26
语法和操作 352ff
语法错误恢复 102
递归 45 , 223 , 277ff 另请参阅 尾递归
算法较差的程序 280
延续传递风格 279
和函数式编程 535 , 538
迭代和 277ff
相互 131
在方案 545ff中
递归下降 73ff , 131
属性管理  c -50
回溯  c -118
短语级恢复  c -2ff
vs 表驱动的自上而下的解析 86
在方案 543中
重新申报 
输出重定向 708ff
精简指令集计算机。  请参阅 RISC
减少 576 , 579
冗余消除。  另请参阅 公共子表达式消除
全球  c -312ff
归纳变量  c -325
本地  c -304ff
可重入代码 663
引用计数 390ff c -146;c -161ff
圆形结构 391
vs 跟踪集合 396
变量参考模型 230ff , 238 , 295ff , 349 , 377 , 379ff , 402 , 425 , 476 , 498ff , 646
执行 情况
参考 
在 C++  467中
前进 193 c -27
在 PHP  467
子程序 152
和值 224 , 230ff
引用 环境116,126
绑定和 152ff
参照透明度 230 , 581 c -274
抽象细化 473
反射 144 , 611 , 791 , 810 , 837ff c -44
在 C#  841中
和动态类型 837 , 842
和泛型 839 , 841 c -127ff; c -291ff
在 Java  838ff中
陷阱 838
在 Prolog  611中
正则表达式库包 743
寄存器  c -61
时钟  c -114
调试 847
FP。  请参见 帧指针
内存层次结构和  c -61
PC(程序计数器)  c -62
处理器状态  c -62;c -72
保存/恢复 414ff
SP。  请参见 堆栈指针
虚拟 776 c -93;c -299;c -307ff
在 x86 和 ARM  c -82ff中
寄存器分配 419 , 780 , 787ff , 824 , 826 c -93ff; c -344ff
复杂度  c -298
图形着色  c -344;c -355
和指令调度  c -95ff; c -299; c -347
在部分执行跟踪 834
基于堆栈的 787
寄存器间接寻址模式  c -72
寄存器干扰图  c -344
寄存器压力  c -299
公共子表达式消除和  c -302;c -320ff
归纳变量消除和  c -326
指令调度和  c -331
循环重新排序和  c -340
软件流水线和  c -335
寄存器重命名  c -78;c -92ff;c -96;c -329;c -352
寄存器溢出 788 c -93ff;c -332;c -347
注册窗口 419ff c -177ff
寄存器内存架构(CISC 机器)  c -71
寄存器-寄存器体系结构(RISC 机器)  c -71;c -93
则表达式 3、44ff、743ff请参阅模式匹配
先进 743
基本 743
编译 746 , 749
从 DFA c -14ff创建 
扩展 743ff
贪婪 748,769
grep  48 , 743ff
克莱恩星 43 , 46 , 49
最小匹配 717 , 748 , 769
嵌套结构,缺少 48
括号 46
在 Perl  745ff中
POSIX  743ff
正则表达式库包 743
脚本语言 704 , 743ff
转换为 DFA  58ff
转换 NFA 746、749
正则语言(集合)  45 , 103
物化 841 c -128; c -291
重定位、链接和 791、796ff816
整数除法中的余数 345
再物化  c -347
远程过程调用 (RPC)  638 c -249ff
执行 809 c -251ff
隐式消息接收 638 c -244
消息调度程序  c -251
参数  c -250
语义  c -249
远程调用发送  c -240
会合  c -249
重复循环。  请参阅 循环、逻辑控制
重复继承。  参见 重复继承
复制继承。  参见 继承、重复、复制
托马斯代表 215
保留字。  请参阅 关键字
分辨率,逻辑编程 592 , 595ff
管道资源危害  c -89
函数返回值 120438ff538
反向分配 511
反向 执行
逆波兰表示法 211ff 另请参阅 后缀表示法
Rexx  700 , 702 , 720 , 771
上下文,使用 703
错误检查 751
作为扩展语言 701
Object Rexx  720
标准化 703
莱斯大学 698
赖斯,H.戈登 113
最右边(规范)推导 51 , 72 , 90ff
RISC(精简指令集计算机)  217 c -60;c -71;c -77ff另请参阅 Alpha 架构ARM 架构MIPS 架构PA-RISC 架构Power 架构SPARC 架构
延迟分支  c -90
延迟加载  c -92
历史  c -100
c -77ff的实现 
加载-存储(寄存器-寄存器)架构  c -71;c -93
c -78的哲学  ;c -100
流水线  c -75
注册窗口 419ff c -177ff
里奇,丹尼斯 M.  9861
RMI。  请参阅 远程方法调用
罗宾逊,约翰·艾伦 595 , 620
Rosetta (苹果) 二进制翻译器 829 , 831 , 832
罗瑟,J.巴克利 584 c -217
Church-Rosser 定理  c -217
朗兹,威廉 C.  215
菲利普·鲁塞尔 591 , 621 , 867
行主布局(数组)  368ff c -337ff
行指针布局(数组)  369ff
RPC。  请参阅 远程过程调用
RPG  700
RTF(富文本格式)  c -151
RTL(GNU 寄存器传输 语言)782、806 c -274ff
鲁宾,弗兰克 295
Ruby  7、12、700、722ff 731、867​
访问控制 762
阵列 754
区块 250 , 722ff
规范实现 703
字母大小写 
类层次结构 497
胁迫 
延续 250 , 448
已删除方法 488
异常处理 441 , 704 , 724
可执行类声明 763
一等子程序 739
“强制退出”示例 722ff
哈希 754
if语句 253
迭代器 268 , 273 , 704 , 722ff
lambda 表达式和过程 159 , 250 , 273 , 294 , 538
换行符 722
方法绑定 507
混合继承 516 , 723
模块 142、719、723
多级返回(捕获抛出)  248 , 249 , 443
多路分配 236 , 704
嵌套块 739 , 768
嵌套子程序,缺少 739
数字类型 719 , 753
对象 141472522704722757ff762ff
参数传递 425
多态性 302
在 Rails  700 , 722
反射 704
正则表达式 48 , 724 , 743
范围规则 739
切片 704
污点模式 853
线程 451
类型检查 183 , 300 , 332 , 703 , 751ff
无限 范围
变量作为参考 498 , 752 , 757
规则,在 Prolog  593
定义 807
拉塞尔,布莱恩 846
罗素,劳福德 J.  468
锈蚀 867c - 250
通信模型 636
存储管理 124 , 378 , 388

年代

S

S 脚本语言 718
S 属性属性语法。  请参阅 属性语法、S 属性
S-DSM。  请参见 软件分布式共享内存
S-表达 547 c -274
SAC(单一任务 C)  537 , 582 , 868
安全性与性能 182 , 241 , 285 c -325
乔尔·萨尔茨  c -354
沙盒 737 , 833ff , 853 , 854
桑顿,莉迪亚 771
萨瑟 469
可满足性  c -234
Scala  867
期货 685
垃圾收集 378 , 390
泛型 333
制约因素 335
对象 472
选项类型 304
线程 451
特征(接口)  509 , 516 , 520
类型推断 325
并行系统的可扩展性 643 , 679
span epub:type="pagebreak" id="pg947" title="947"/>标量类型。  请参阅 类型、标量
扫描仪生成器 56ff , 62 , 104 另请参阅 lex
扫描仪和扫描 32728ff4454ff
特设 
具有显式有限自动机 55 , 61ff
词汇错误 65ff
最长可能 标记规则55、64ff
非平凡前缀问题 64
表驱动 65ff
分散操作  c -244;c -249;c -251
基于调度程序的同步 636 , 665ff
实现 663ff
在多处理器 651
在单处理器 648ff上
Scheme  539ff , 868 另请参阅 Lisp
应用函数 548ff
作业 545ff
关联列表 545
绑定 542ff
布尔常量 544
条件表达式 545 , 584
延续 250 , 291 , 448
控制流 545ff
柯里化 580
延迟和强制 282 , 547 , 570 , 686
指称语义 215
DFA 示例 548ff
平等测试 341 , 544ff
eval函数 548ff
评估令 567ff
表达式类型 569
一等函数 156 , 431 , 739
迭代 272
for-each  546ff
不精确常数 106
lambda表达式 159 , 541 , 577
惰性求值 586
构造 132 , 542
列表 543ff
宏 164 , 569 , 571
映射函数 576
嵌套子程序 127
数字和数字函数 305 , 544 , 719 , 753
操作语义 215
程序作为列表 547ff
引用 540
有理类型 305
递归 545ff
递归声明 543
S 表达式 547
范围规则 125 , 132 , 134 , 142 , 412 , 543 , 739
搜索 545
自定义 548ff
自我表征(同 形像125、547ff608
特殊形式 283 , 292 , 541 , 569
符号 541
类型检查 703 , 751
类型谓词函数 541
无限 范围
变量作为引用 752
谢勒,威廉 N. III,v 
希夫曼,艾伦M.  530 , 854
施耐德,弗雷德 B.  697
斯文·博多·舒尔茨 583 , 868
肖尔,赫伯特 394 , 410
施瓦茨,雅各布 T.  409 c -355
科学记数法和浮点数  c -67
范围 3 , 116 , 125ff 另请参阅 绑定绑定规则特定语言
关闭 138
动态 125 , 142ff , 300 , 535
替代方案 172
概念模型 742
动机 
全局变量 126
128 , 132孔 
实现 128 , 144 , 412 c -26ff另请参阅 显示静态链接
关联列表 144 c -31ff
中央参考表 144 c -31ff
符号表  c -26ff
词汇。  参见 作用域、静态
局部变量 126
开放 138 , 476
脚本语言 702 , 739ff
静态 125ff
139ff488ff类 
申报令 130ff
声明和定义 133ff
模块 135ff , 486ff
嵌套块 134ff , 542ff c -26
嵌套子程序 127ff c -26
自身(静态)变量 127、136
脚本语言739ff中未声明的变量 
范围解析运算符 128 , 477 , 489 c -197
符号表c的作用域堆栈  -27ff
Java 中的作用域内存 390
斯科特,达纳 S.  113 , 215
阿隆佐教堂和 537
斯科特,迈克尔 L.,第 691 , 693 , 698 节
脚本语言 12,699ff请参阅AppleScriptawkbashEmacs LispJavaScriptLuaMapleMathematicaMatlabPerlPHPPowerShellPython RRexxRubySSchemesedTcl;Tk VBScript Visual BasicXSLT
访问系统设施 703
数组 362 , 367
特性 701ff , 738ff , 765
数据类型 704ff751ff
复合材料 753ff
数字 752
声明 702
定义 699
动态类型 118 , 703
一级子程序 155
“强制退出”示例 
在 Perl  718ff中
在 Python  720ff中
在 Ruby  722ff中
垃圾收集 124 , 378
通用 718ff
胶水语言 718ff
历史 700ff
交互式使用 701
魔法数字 712
数学 717ff
模块 137
名称 739ff
面向对象 757ff
模式匹配 559 , 704 , 743ff
问题域 704
引用 709ff
反射 611 , 837 , 841
报告生成 712ff
范围规则 702 , 739ff
shell 语言 705ff
统计 717ff
字符串操作 367 , 375 , 704 , 743ff
文本处理 712ff
类型检查 513
无限 延伸
变量扩展和插值 706ff , 748ff
万维网和 727ff , 835
CGI 脚本 728ff
客户端脚本 734ff
Java 小程序 734ff
服务器端脚本 727ff
XSLT 和 XPath  c -261ff
shell 的搜索路径 707
塞贝斯塔,罗伯特·W.  41
二等子程序 155ff
二级缓存  c -61
数组的一部分。  请参阅 数组、切片
安全 737 另请参阅 沙盒
和二进制重写 850
代码注入  c -176
公共语言基础设施  c -288;c -291
vs 功能 737
Java虚拟机 813
JavaScript  737
和反思 838
堆栈粉碎 385
污点模式 853
sed  700,713ff , 866
HTML 标头提取示例 713
单行脚本 713
模式空间 713
正则表达式 48 , 743
标准化 703
寻道操作,针对文件  c -151
分段记忆 378
段 791
汇编程序中的段切换 795
彼得·西贝尔 862
选择例程(POSIX)  c -118
选择 223,253ff
case/switch语句 256ff
if语句 253
在 Prolog  605中
短路条件 254ff , 285 , 287 , 288
选择函数。  请参阅 SSA 形式的合并函数
自我(语言)  529 , 530 , 757
自我定义。  参见 同像语言
自托管编译器 21
自修改代码 829
自调度  c -344
语义动作例程。  参见 动作例程
语义分析 32ff , 179ff 参见 属性语法
断言 182ff
不变量 183 , 290 , 295
前置条件和后置条件 183
符号表和 32
语义检查 18,179请参阅语义错误
动态 144 , 181ff , 410 c -32
算术溢出 243 , 318
数组下标 364 , 373 , 833 c -326
悬垂参考 389 , 833
禁用 182
在链接器 803中
反向分配 511
安全性与性能 181 , 241
子范围值 310 , 325ff c -326
类型转换 316ff , 320 , 511
未初始化变量 239 , 287 , 833
变体记录  c -141
静态 181
语义错误 
动态。  另请参阅 语义检查类型冲突
惰性求值 569
case语句 中缺少标签259
空指针取消引用 244 , 400
未处理的异常 443
静态 102
不明确的方法引用  c -197;c -201
属性语法处理 203
声明隐藏本地名称 134
C++ 中的无效泛型强制转换  c -134
缺少默认构造函数 498
case语句 中缺少标签260 , 562
C++ 511中缺少 vtable 
Pascal 227中优先级的误用 
Pascal 464中的子范围和打包类型作为参数 
取非左值的地址 319
ML 328中的类型检查 
通用338上不支持的操作 
声明前使用 131
语义功能 185ff 参见 属性语法
符号 186ff
语义钩子  c -47
语义堆栈  c -53
语义 3,44请参阅属性 语法
公理 182 , 215 , 290 , 591
符号 214 , 215 , 250 , 300 , 301 , 351
动态 32 , 143 , 179 另请参阅 语义检查语义错误
监视器 671ff
业务 运作
远程过程调用  c -249
静态 32 , 143 , 179
vs 语法 43ff
二进制 667
有界缓冲区示例 668
发送操作  c -239ff
缓冲  c -241ff
模拟替代方案  c -243ff
错误报告  c -242ff
同步语义  c -240ff; c -243ff
语法  c -243
逆向屏障 656
句子形式 51
哨兵值 287 , 295
单独编译 117 , 165ff , 797 , 806 c -36ff
在 C  137
在 C++  477中
和代码改进 778
历史  c -41
和模块 140
脚本语言 739
类型检查 799ff
测序 223,252ff
顺序一致性 659ff
顺序文件  c -150
序列化 
在并发程序中 656
消息中的对象数 837
交易量 680
服务器端 Web 脚本 727ff
449ff中的setjmp例程
214,409号 ​
集合 376ff
复合类型 311
执行 情况
在 Python  755中
SGML(标准通用标记语言)  c -258ff
上海市 700,705
标准化 703
浅绑定。  请参阅 浅绑定规则
Lisp c中名称查找的浅绑定  -33
浅层比较和分配(副本)  340ff
沙米尔,埃利亚胡 113
数组的形状。  请参阅 数组、形状
共享继承。  请参阅 重复继承、共享继承
共享内存(并发)  652ff 另请参阅 同步
与消息 传递631、635ff相比
调度程序实现 663ff
夏普,奥利弗 J.  c -355
沙斯塔 295
沙维特,尼尔 698
shell  700、705ff718请参阅bash命令解释器脚本语言
谢里丹,迈克尔 812
移位归约解析。  请参阅 自下而上解析
Shirako,  698 年6 月
SHMEM 库包 697
短路评估 192 , 243ff , 254ff , 282 , 285 , 287 , 288 , 584
副作用 12 , 234 , 281ff , 607 , 616ff
和赋值运算符 235
和汇编 582
和并发 685
定义 229
异常处理程序 446
和函数式编程 537ff , 581
和输入/输出 571
指令  c -347
和惰性求值 283 , 569ff c -182
和宏 163
和不确定性 286 c -114
并排序 241ff252ff571
在 RTL  c -276
在方案 545ff中
Smalltalk 作业  c -205
声明 303
子程序调用  c -304
Siewiorek,Daniel P.  c -105
信号 
在显示器 670ff中
级联 672
暗示与绝对 671ff , 674
操作系统 101、457ff647、650ff658​​
蹦床 457
信号 NaN,浮点 239 c -70
浮点数的有效数字  c -67
重要注释。  请参阅 pragma
硅谷图形公司 633 , 634
简单类型。  请参阅 简单类型
模拟 177 , 469 , 868
仙人掌堆 454
按名称调用参数 433 c -180ff
课程 141
协程 451
分离操作 452、455、647
封装 472 , 486
继承 
方法绑定 507
对象初始化 502
参数传递  c -180ff
类型系统 301
变量作为引用 498
虚拟方法 508
模拟 456,830c - 187ff​
单一作业 C  537 , 582 , 868
单一继承 510ff
单步执行 846ff
单精度浮点数  c -63; c -68
西普瑟,迈克尔 113
剑麻 11 , 537 , 546 , 582ff , 683 , 868
汇编 582
斯科勒姆,托拉尔夫  c -227
斯科莱姆化  c -227;c -230ff
数组切片 360ff , 718 c -343
SLL 解析。  请参阅 解析器和自上而下的解析
肯尼斯·斯隆内格 215
SLR 解析。  请参阅 解析器和自下而上的解析
Smalltalk  7、12、177、472、522ff 669、868c - 204ff
拟人化 497 , 523
作业  c -205
结合性和优先级,缺乏  c -205
块  c -205
类层次结构 497
课程 141
控制抽象  c -206ff
表达式语法 422 c -204
如果构造 422 c -205
实现 23,530
中缀表示法 226
继承 
解释 18
迭代 272 c -205ff
消息  c -204
元类 497
方法绑定 507
多重继承 530
对象初始化 496ff
对象超类 479
面向对象编程和 523 c -204ff
参数传递 425
多态性 302
优先权 227
编程环境 25ff , 41 , 523 c -148ff; c -204ff
自身 473 c -207
超级 480
类型检查 183 , 300 , 332 , 512
无限 延伸
变量作为参考 231 , 498
类成员的可见性 489
智能指针 391 c -146; c -161
史密斯,詹姆斯 E.  c -105
史密斯,兰德尔 B.  530
SML  326 , 550 , 865 另请参阅 ML
平等测试和排序 554
泛型(函子)  334
制约因素 335
中缀表示法 422
斯内林格,WJ  348
斯诺博尔 868 c -118
模式匹配 559
范围规则 125 , 142
自我表征(同形像)  608
侦听(缓存一致性)  633
斯奈德,艾伦 469
斯奈德,劳伦斯 410
肥皂 530,698
逻辑工具协会 863
插座  C -235
软件流水线  c -333ff
所罗门,马文 H.  295
SPARC 架构633、806 c -99ffc -105
寻址模式  c -72
原子指令 655
条件代码  c -73
字节序  c -64
围栏说明 661
记忆模型 662
注册窗口 419 c -177ff
方案283、292、541、569、695的特殊形式 ​
猜测 223
代码缓存 823
代码改进 184
或并行 686
处理器执行  c -79;c -89;c -104
交易 680ff
寄存器溢出。  请参见 寄存器溢出
自旋锁 653ff , 664ff
和抢占 693
两级 690
自旋然后让出锁 664ff
旋转。  请参阅 同步、忙等待
电子表格 12
SR  869
案件陈述,缺乏  c -111
受保护的命令  c -110;c -117
执行 803
声明  c -113
顺序和并发构造的集成 647
信息筛选  c -114
不确定性  c -110
线程 638
斯利瓦斯塔瓦,阿米塔布 854
SSA 表格。  请参阅 静态单一分配表格
堆栈帧 120ff , 412ff c -167ff
簿记信息 120
使用动态形状数组 366
返回值 439
临时工 120
参数数量可变 437
堆栈指针 121 , 412
堆栈粉碎 385
基于堆栈的分配和布局 120ff412ff792 另请参阅 调用序列堆栈框架
回溯 250 , 848 , 853 c -172
对于协程 453ff
例外 447ff
对于迭代器  c -184
嵌套线程。  请参阅 Cactus Stack
基于堆栈的中间语言 782 , 811 参见 Forth字节码Postscript
优化 812
处理器管道停顿。  请参见 管道停顿
标准输入和输出 708
标准 ML。  请参阅 SML
标准化 8
斯坦福大学 10,468 c -105
斯坦西弗,瑞安·D.  590
CFG 49的起始符号 
陈述 
vs 表达 224 , 229 , 233
谓词演算 612 c -226
静态绑定。  请参阅 绑定、静态
静态链 130ff , 413
vs 显示 417
维护 
静态链接,子程序 129 , 413
静态链接器 797
静态方法绑定 506
静态对象, 存储分配119ff127、136
静态作用域。  请参阅 作用域、静态
静态语义。  请参阅 语义、静态
静态单一分配 (SSA) 形式 825 c -274;c -312ff;c -355
静态类型。  请参阅 类型检查、静态
stdio库,在 C/C++  436 c -154 中;c -156
斯特恩斯,理查德·E.  112 , 215
斯蒂尔,盖伊·L·Jr.  539 , 862 , 868
停止并复制垃圾收集 394ff , 410
垃圾收集中的 Stop-the-World 现象 396
存储分配和管理 118ff 另请参阅 悬垂引用垃圾收集内存泄漏
储存压实 123 c -146
store_conditional指令 657 , 680 , 691
存储程序计算 12
斯托伊,约瑟夫 E.  215
斯特雷奇,克里斯托弗 215
流 
在 C++ 中  c -156ff
在函数式编程中 571ff , 586
和 Unix 工具 744
强度降低  c -302; c -325
严格的语言 
严格名称等价性 315
严格性分析 582
字符串 311 , 375ff
在 ML  556
存储管理 376
露天开采  c -343
斯特罗姆,罗伯特·E.  347
强类型语言。  请参阅 类型检查、强
Stroustrup,Bjarne  346 , 530 , 861 c -194 ;-197
类型313ff406560799的结构等价性 
类型结构视图 300
结构化编程 7 , 246ff , 295 c -324
goto备选方案 247ff , 285
结构 351ff 另请参阅 记录
在 Prolog  593中
存根 
对于动态链接  c -281
在 RPC  843 c -249
样式表语言  c -259
子类。  请参阅 类、派生类
次正规数  c -68
子范围类型 309ff , 324ff
子程序 223、411ff请参阅调用序列控制抽象函数叶例程 参数传递;参数;堆栈框架基于堆栈
分配和布局 
闭包。  请参阅 闭包、子程序
尾声 120 , 414
一等。  请参阅 一等值、子程序 另请参阅 函数、高阶
正式 431 另请参阅 一等值、子程序
帧指针 120 , 121 , 412
通用。  请参阅 通用子程序或模块
对代码生成和改进的影响 476 c -96ff
内联。  请参阅 内联子程序
嵌套。  请参阅 子程序嵌套
面向对象编程 477ff
在 C 432中,指向 
序言 120 , 414
参考 
二等 155ff
堆栈指针 121 , 412
静态分配 119
子类型 315 , 320 , 505 另请参阅 类、派生
受限 310
亚型分析 184 , 510
亚型多态性。  参见 多态性、亚型
成功,计算图标 305 c -108
Sun Microsystems, Inc.  530 , 736 , 812 , 864 另请参阅 Oracle Corporation
桑德尔,哈坎 696
740卢比的超级分配 
超类。  参见 类、基类
超标量处理器 625 c -78ff; c -355ff
苏拉斯基,泽夫 866
萨斯曼,杰拉尔德·杰伊 178 , 539 , 590 , 868
斯威尼,彼得 F.  530
Swift  869
关联数组 359
换行符 48
混合继承 516
嵌套子程序 127
对象 472
可选类型 304
参数传递 423 , 424 , 435
范围 规则
集合 377
结构 351
类型检查 332 , 513
类型推断 325
变量作为引用或值 498
摆动  c -149
switch语句。  请参阅 case/switch语句
符号表 32 , 34 , 144 , 787 c -26ff
和行动例程 200
和属性语法 787
和动态链接 809
和解释 845
在 Java类文件 814、816
语法树相互递归 380
在目标文件 36791829837ff中
传递继承属性 188
范围堆栈  c -27ff
语义分析器 32,776
外部符号 791
Symbolics 公司 41
西姆,唐 349 , 863
在 Ada  674ff中
阻塞。  请参阅 同步、基于调度程序
忙等待( 旋转636、653ff665
屏障 655ff
和抢占 693
自旋锁 653ff664ff690
条件同步 652 , 666ff
定义 636
事件处理程序 458 , 658 c -148
粒度 679
隐含的 683ff
指令 660ff
在 Java  676ff中
在消息传递中  c -240ff
非阻塞 657ff , 696
同步发送  c -240
Java 类的 同步方法676
句法糖 148 c -157
Ruby 中的数组访问 755
数组下标 360 , 385
定义 148
Prolog 中的平等 595
C# 3.0 中的扩展方法 494
OCaml 中的 函数561
迭代 270 , 273
Ruby 中的方法调用 722
单子 572
Perl 中的面向对象 758
运算符重载 148 , 225 , 236
术语起源 
JavaScript 中的属性 755
Ruby 中的正则表达式 724
同步方法 676
尾递归 546 , 582 , 684
bash  707中测试
语法 3、29、43ff请参阅上下文无关文法则表达式
递归结构和 45
vs 语义 43ff
语法分析。  参见 解析器和解析。 参见 扫描器和扫描。
语法 错误75,102ff c -1ff
自下而上恢复  c -10ff
上下文特定的前瞻 102 c -3ff; c -7
错误产生 103
拼写错误  c -25
紧急模式恢复 102 c -1ff; c -10ff
短语级恢复 102 , 113 c -2ff
修理  C -7
语法树,摘要 33 , 34 , 69 , 198
属性语法和 193ff , 201
平均示例 804
c -93的基本块  ;c -304
在 C#  826中
组合示例  c -305ff
180ff193ff198的构造 
33180ff201ff的修饰(注释) 
GCD 示例 33ff776ff
和本地代码改进  c -310
带符号表的相互递归 380
vs 分析树 77
在 Perl  827中
语义分析和 180ff
语义错误和 203ff
语法树,具体。  参见 解析树
语法制导翻译 69
合成属性。  请参阅 合成属性

电视

T

表驱动解析。  另请参阅 解析器和解析
自下而上 95ff
自上而下 82ff
表驱动扫描 65ff
变体记录的标签 238 c -137
尾递归 279ff , 546 , 582 , 646 c -325
Perl 和 Ruby 中的 污点模式853 c -354
塔南鲍姆,安德鲁 S.  348
目标代码生成编译 阶段27,34ff
任务 635
.NET中的任务并行库 (  TPL ) 626、639、684
任务并行 643
Tcl(工具命令语言)  ​​702 , 869 另请参阅 Tk
数组和哈希 755
上下文,使用 703
错误检查 751
作为 扩展语言701、720
增值Tcl  720
嵌套子程序 739
数字类型 752
范围规则 125142702739ff
自我表征(同形像)  608
变量作为值 752
TCP 互联网协议 687 c -237;c -242
泰特尔鲍姆,蒂莫西 215
模板元编程 163 c -120
模板,在 C++ 中 331、333ff337ff483 c -119ff另请参阅通用子程序或模块
可变参数 348
时间循环,在放松记忆模型 660ff中
临时文件 401 c -150
在 Prolog  593中
CFG 49航站楼 
分布式系统中的终止  c -246ff; c -255
test_and_set指令 654ff , 666
特10  , 24 , 769 , 783
动态作用域 142
文本文件。  请参阅 文件、文本
脚本语言中的文本处理 712ff
定理证明,自动化  c -230
参数 487ff
托马斯·大卫 295 , 867
汤普森,肯尼斯 9 , 306 , 705 , 744 , 863
汤普森壳 705
线程 635 另请参阅 并发上下文切换多线程调度
协程和 452
創作 638ff
共同开始 638ff , 642
早期答复 
隐式接收 646 c -244另请参阅 远程过程调用
在 Java 2  643
在 Java 5  645ff中
启动时详述 641ff
并行循环 639ff
事件处理 459
实现 647ff , 809
抢占 650ff
vs 进程 635 , 647
堆栈框架。  另请参阅 Cactus Stack
函数式程序中的状态线程 573
三地址指令  c -71
thunk  468 c -180ff; c -250
Prolog 600ff605、608ff的井字游戏示例 
蒂奇,沃尔特 F.  806
数组/循环平铺  c -337
消息传递超时  c -246
时间窗口 651 另请参阅 竞争条件
钛 698
701,869塔卡 ​
令牌 19、28、44ff请参阅扫描扫描
示例 28
拼写错误  c -25
前缀 64ff
正则表达式和 45ff
墓碑(用于悬空引用检测)  410 c -144ff;c -161
引用计数 391 c -146
500强榜单 696
自上而下的解析。  请参阅 自上而下的解析
拓扑排序 618 c -330
Torczon  Linda 41,215,806 c -106 ;c -354​
跟踪调度 184 , 831
跟踪垃圾收集 393ff
vs 引用计数 396
LR 生产200 c -49的尾部 
蹦床 
在闭包实现中  c -174;c -176
信号处理 457
事务内存 589 , 679ff c -100
有界缓冲区示例 680
挑战 
实现 681,809
非阻塞 696
重试 680 , 682
协程454ff的传输操作 
过渡函数 
有限自动机  c -13
LR 系列解析器 94
下推式自动机  c -18
台式扫描仪 65
翻译方案 191 另请参阅 属性流属性语法
ad hoc  198 另请参阅 action 常规
全美达公司 829
透明度  c -249
电脑,INMOS  866
陷阱指令 847
胎面痕迹 698
树语法 202ff , 781
与上下文无关文法 203
64位字符 
简单更新问题 582
真正的依赖。  参见 依赖、流动
元组 236 , 311
在 ML  552 , 557
图灵(语言)  869
别名 176
模块 137
副作用 242 , 252
图灵,艾伦 536ff
阿隆佐教堂和 537
图灵完备性 8 , 569 , 620 c -120
图灵机 536
特纳,大卫 A.  865
二进制补码运算 243 , 317 c -65ff
双地址指令  c -71
两级锁定 690
221型 
别名 315
匿名 316
布尔值 305 另请参阅 短路评估
内置 300ff
红衣主教 305
分类 305ff
综合体 305
复合 300 , 310ff 另请参阅 数组文件列表指针记录变体记录集合
脚本语言 704ff753ff
构造。  参见 类型、复合
衍生 315
离散 257 , 306
枚举 307ff
逻辑 305
数字 305ff 另请参阅 浮点
任意精度(大数)  753
十进制 307 c -86
定点 305
多长度 309
有理数 305 , 753
在方案 544中
脚本语言 752
未签名 305
不透明 138
序数。  参见 离散类型
包装 354 , 464
预定义 300
原始 300
递归 131 , 311 , 349 , 377ff
标量 307
自我描述 323
简单 307
子范围 309ff , 324ff
通用参考 323ff
匿名 316
类型转换 316ff , 431
动态 319,511ff
静态。  请参阅 类型转换
类型检查 299 , 312ff 另请参阅 强制类型等价类型兼容性类型推断
带属性语法 201ff
动态 143,300ff
脚本语言和 118 , 703
与静态 300相比
逐步 
链接和 799ff
在 ML  326ff328ff中
另行编译 799ff
静态 299
强 299
垃圾收集 391
和通用参考类型 323ff
类型冲突 144 , 204ff , 299 , 308
Java 和 C# 中的相等性测试 234
在方案 541中
在 Smalltalk  513
类型类,在 Haskell 中为 348 , 554
类型兼容性 298312320ff756ff
类型一致性 348
类型一致性,ML  328
类型约束 310
对于泛型 335ff c -128
类型构造函数 116 , 304 , 310
类型转换 312 , 316ff , 511
类型描述符 
用于动态类型和方法调度 513
用于垃圾收集 391 , 394
反向分配 511
类型等价 298 , 312ff
名称 313ff , 799
结构 313ff , 406 , 560 , 799
类型擦除  c -126ff
类型扩展 471 , 491ff , 524 , 865 另请参阅
类型层次分析 529
类型推断 298 , 312 , 324ff , 621 , 756ff , 865
在 ML  326
用于静态方法调度 530
对于子范围 324ff
Scheme 中的类型谓词函数 541
类型传播 529
类型转换。  请参阅 类型转换、非转换
类型系统 298ff 另请参阅 多态性类型检查
定义和观点 300ff
正交性 302ff
多态性和 302ff
目的 
类型变量 329
Perl 中的类型团块 754
TypeScript  348
类型状态 347

U

UCS(通用字符集)  306 另请参阅 Unicode
UDP 互联网协议  c -237;c -242
厄尔曼,杰弗里·D.  112
未声明的变量,脚本语言中的范围 739ff
昂格尔,大卫 530
Unicode  47、305ff375
字符实体 306 c -264
整理元素 745
统一 
在 C++ 模板中 331
成本 349
统一并行 C (UPC)  698
统一对象模型 522
未初始化变量 238 , 239 , 791 , 833
工会。  参见 变体记录
单元号(在 Fortran I/O 中)  c -152
全称量词  c -226ff
通用参考类型 323ff , 345
宇宙类型,集合 376
艾克斯-马赛大学 591 , 621 , 867
亚利桑那大学 864 , 869
加州大学伯克利分校  c -105
加州大学洛杉矶分校 537
爱丁堡大学 591 , 621 , 867
赫特福德大学 868
伊利诺伊大学 
厄巴纳-香槟 418
罗切斯特大学 
多伦多大学 869
威斯康星大学麦迪逊分校 
Unix  20 另请参阅 Linux
和 C  861
grep ,则表达式和 48,744
神奇数字 712 , 820
profgprof  848
ptrace  846
SunOS  806
文本文件  c -151
无限范围。  参见 “物体的范围”
未命名类型。  请参阅 类型、未命名
安鲁,埃尔文 349
无符号整数 
语言级别 305
机器级  c -65
展开子程序调用堆栈 
例外情况 249 c -5
在非本地goto  248、249 c -181
UPC(统一平行C)  698
监视器671的紧急队列 
URI(统一资源标识符) 
小程序 735
CGI 脚本 728ff
XSLT 中的操作  c -267
在 Perl  731中
与 URL  727相比
无用指令  c -303;c -321
CFG 52中的无用符号 
UTF-8 字符编码 306

V

Val( 语言11,546
Valgrind  176
变量价值模型 230ff , 377 , 402 , 423 , 425 , 476 , 498ff
值编号  c -355
全球  c -312ff
本地  c -307ff
值 
左值 230 , 311 , 319 , 323 , 384 c -48
r 值 230 , 384 c -48
以及参考文献 224 , 230ff
范罗苏姆,吉多 720 , 867
范韦恩加登,A.  859
变量。  另请参阅 初始化;变量的名称引用模型作用域变量的值模型
在 CFG  49
脚本语言中的插值(扩展)  706ff748ff
逻辑编程 592 , 593 , 595
语义分析和 33
未声明,脚本语言中的范围 739ff
可变参数模板 348
向量c的方差  -93ff
变体记录(并集)  238 , 319 , 351 , 357ff c -136ff
复合类型 311
限制性  c -141
垃圾收集 391
与继承 358
内存布局  c -141
在 ML  558
与非转换强制类型转换 358
安全  c -138ff
标签检查 184 c -141
VAX 架构 830
参数指针 463
字节序 344
子程序调用 463 c -193
矢量指令  c -61;c -84
矢量处理器 625、640、683 c -342ff ;c -355
很忙的表情  c -351
VES(虚拟执行系统)。  请参阅 通用语言基础设施
VEST二进制 翻译830,854
CFG c -19的活前缀 
物体  c的视图-194
虚拟机 xxv19、810ff854另请参阅公共语言基础结构Java 虚拟机
vs 解释 17 , 18 , 810
特定语言 810
流程与系统 811
Smalltalk  854
虚拟机监视器 (VMM)  811 c -85
虚拟方法表 509ff
虚拟方法。  请参阅 方法和方法绑定、虚拟
虚拟寄存器 776 c -93;c -299;c -307ff另请参阅 寄存器分配
和可用表达式  c -317ff
和活变量分析  c -321
和值编号  c -307; c -312ff
面向对象语言中成员的可见性 479 , 488 c -27; c -30
访问者模式 294 , 514
Visual Basic  861 c -286另请参阅 Basic VBScript
和脚本 724ff , 835
Visual Studio  25
超大规模集成电路 217 c -77
VMWare  811
挥发性变量 450 , 662 , 678
冯·诺依曼,约翰 12
冯·诺依曼语言 12
vtable(虚拟方法表)  509ff

西

W

瓦德勒,菲利普 583 , 590
瓦赫贝,罗伯特 854
韦特,威廉H.  394 , 410
瓦尔德玛,塞莱斯 864
沃尔,拉里 700 , 716 , 866
万德·米切尔 295 , 469
观察点 845ff
瓦特,大卫 A.  215
网络浏览器 627ff
脚本 701 , 724 , 727 , 734ff
网络爬虫 770
韦格曼,马克 N.  c -355
彼得·韦格纳 178 , 349 , 530 c -225
彼得·温伯格 714
韦瑟,马克 410
韦斯,什洛莫  c -105
威尔士,吉姆 348
韦特斯坦,H  . 693
while循环。  请参阅 循环、逻辑控制
白色空间 47
通配符,在 shell 语言中 706
威廉·莱因哈德 590 c -209
威尔逊,保罗 R.  410
威尔塔穆斯,斯科特 861
温奇,大卫 771
Windows 操作系统 22 另请参阅 .NET
在 Alpha 架构 830上
目标文件格式  c -291
文本文件  c -151
线程包 637
Windows 演示基础 (WPF)  459
温斯克尔,格林 215
尼克劳斯·维尔斯 8 , 21 , 41 , 81 , 103 , 113 , 140 , 177 , 246 , 294 , 307 , 861 , 865 , 866 c -2
维斯尼夫斯基,罗伯特 693
沃尔夫,迈克尔  c -355
工作区(例如在 Smalltalk 或 Common Lisp 中)  c -149
万维网。  另请参阅 脚本语言、万维网和
角色模型标准 349
并行计算的发展 637
以及脚本的发展 533 , 700 , 764
安全 737
万维网联盟 727 , 734 , 736 , 771 , 869
越差越好 764 , 771
WPF(Windows 演示基础)  459
写入缓冲区 659
写-读依赖关系。  参见 依赖关系、流程
写-写依赖性。  请参阅 依赖性、输出
WYSIWYG(所见即所得)  669

X

X10  698
x64 架构。  请参阅 x86-64 架构
x86架构 22、633、831、834 c -80ff
汇编语言 5 , 34
原子指令 655 , 661
二进制编码的十进制  c -86
调用序列 418 c -171ff
c -88的编译 
条件确定延迟  c -103
条件分支  c -86
调试寄存器 847
动态链接  c -280
字节序  c -64
浮点  c -69; c -82; c -84
机器语言 5
794号 
指令c的前缀代码  -86
寄存器设置  c -82ff
分割 791
字符串操作指令  c -64
双地址指令  c -71
矢量指令 695 c -84
x86-64架构 633、834 c -99
Xamarin 公司 821 c -286
XCode  25
施乐帕洛阿尔托研究中心 (PARC)  41 , 523 , 669 , 863 , 868 c -148; c -204
XHTML  738 , 771 , 869 c -259ff
引用演示示例  c -261ff
XML(可扩展标记语言)  15 , 530 , 737ff , 869 c -151; c -258ff
命名空间  c -261;c -264
树结构  c -261
XML 模式  c -260
XPath  869 c -261ff;c -264ff
XQuery(XSL查询语言)  c -261
XSL(可扩展样式表语言)  737 , 869 c -258ff
XSL-FO(XSL 格式化对象)  869 c -261
XSLT(XSL 转换)  1215620737ff869 c -258ff
书目格式示例  c -262ff

Y

yacc  44 , 94 , 104 , 112
动作例程  c -45ff
错误恢复  c -10ff
杨俊峰  c -354
耶特曼,科里 113
耶林,弗兰克 806
产量,衍生 51
Yochelson,Jerome C.  410
扬格,丹尼尔 H.  69

Z

Zadeck,F.Kenneth  c -355
赵秦 855